blob: 4bd9244a194750f02cb705c36145c50c9516080d [file] [log] [blame]
// Copyright 2008 Google Inc. All Rights Reserved,
//
// Author: Justin Uberti (juberti@google.com)
// Frank Barchard (fbarchard@google.com)
#include <string>
#include "talk/base/flags.h"
#include "talk/base/gunit.h"
#include "talk/base/pathutils.h"
#include "talk/base/scoped_ptr.h"
#include "talk/base/stream.h"
#include "talk/base/stringutils.h"
#include "talk/session/phone/formatconversion.h"
#include "talk/session/phone/webrtcvideoframe.h"
#include "talk/session/phone/testutils.h"
#include "talk/session/phone/videocommon.h"
enum {
ROTATION_0 = 0,
ROTATION_90 = 90,
ROTATION_180 = 180,
ROTATION_270 = 270
};
using cricket::WebRtcVideoFrame;
using cricket::FOURCC_I420;
static const int kWidth = 1280;
static const int kHeight = 720;
static const int kAlignment = 16;
static const std::string kImageFilename = "faces.1280x720_P420.yuv";
DEFINE_int(yuvconverter_repeat, 1,
"how many times to perform each conversion operation (for perf testing)");
class WebRtcVideoFrameTest : public testing::Test {
protected:
virtual void SetUp() {
repeat_ = FLAG_yuvconverter_repeat;
}
public:
// Load a video frame from disk or a buffer.
bool LoadFrame(const std::string& filename, uint32 format,
int32 width, int32 height, WebRtcVideoFrame* frame,
int rotation) {
talk_base::scoped_ptr<talk_base::MemoryStream> ms(LoadSample(filename));
return LoadFrame(ms.get(), format, width, height, frame, rotation);
}
bool LoadFrame(talk_base::MemoryStream* ms, uint32 format,
int32 width, int32 height, WebRtcVideoFrame* frame,
int rotation) {
if (!ms) {
return false;
}
size_t data_size;
bool ret = ms->GetSize(&data_size);
EXPECT_TRUE(ret);
if (ret) {
ret = LoadFrame(reinterpret_cast<uint8*>(ms->GetBuffer()), data_size,
format, width, height, frame, rotation);
}
return ret;
}
bool LoadFrame(uint8* sample, size_t sample_size, uint32 format,
int32 width, int32 height, WebRtcVideoFrame* frame,
int rotation) {
// WebRtcVideoFrame only supporst I420 for now
if (format != FOURCC_I420)
return false;
for (int i = 0; i < repeat_; ++i) {
uint8* new_buffer = new uint8[sample_size];
memcpy(new_buffer, sample, sample_size);
frame->Attach(new_buffer, sample_size, width, height, 0, 0);
}
return true;
}
talk_base::MemoryStream* LoadSample(const std::string& filename) {
talk_base::Pathname path(cricket::GetTestFilePath(filename));
talk_base::scoped_ptr<talk_base::FileStream> fs(
talk_base::Filesystem::OpenFile(path, "rb"));
if (!fs.get()) {
return NULL;
}
char buf[4096];
talk_base::scoped_ptr<talk_base::MemoryStream> ms(
new talk_base::MemoryStream());
talk_base::StreamResult res = Flow(fs.get(), buf, sizeof(buf), ms.get());
if (res != talk_base::SR_SUCCESS) {
return NULL;
}
return ms.release();
}
// Write an I420 frame out to disk.
bool DumpFrame(const std::string& prefix,
const WebRtcVideoFrame& frame) {
char filename[256];
talk_base::sprintfn(filename, sizeof(filename), "%s.%dx%d_P420.yuv",
prefix.c_str(), frame.GetWidth(), frame.GetHeight());
size_t out_size = cricket::VideoFrame::SizeOf(frame.GetWidth(),
frame.GetHeight());
talk_base::scoped_array<uint8> out(new uint8[out_size]);
frame.CopyToBuffer(out.get(), out_size);
return DumpSample(filename, out.get(), out_size);
}
bool DumpSample(const std::string& filename, const void* buffer, int size) {
talk_base::Pathname path(filename);
talk_base::scoped_ptr<talk_base::FileStream> fs(
talk_base::Filesystem::OpenFile(path, "wb"));
if (!fs.get()) {
return false;
}
return (fs->Write(buffer, size, NULL, NULL) == talk_base::SR_SUCCESS);
}
// Create a test image for YUV 420 formats with 12 bits per pixel.
talk_base::MemoryStream* CreateYuv420Sample(uint32 width, uint32 height) {
talk_base::scoped_ptr<talk_base::MemoryStream> ms(
new talk_base::MemoryStream);
if (!ms->ReserveSize(width * height * 12 / 8)) {
return NULL;
}
for (uint32 i = 0; i < width * height * 12 / 8; ++i) {
char value = ((i / 63) & 1) ? 192 : 64;
ms->Write(&value, sizeof(value), NULL, NULL);
}
return ms.release();
}
talk_base::MemoryStream* CreateRgbSample(uint32 fourcc,
uint32 width, uint32 height) {
int r_pos, g_pos, b_pos, bytes;
if (!GetRgbPacking(fourcc, &r_pos, &g_pos, &b_pos, &bytes)) {
return NULL;
}
talk_base::scoped_ptr<talk_base::MemoryStream> ms(
new talk_base::MemoryStream);
if (!ms->ReserveSize(width * height * bytes)) {
return NULL;
}
for (uint32 y = 0; y < height; ++y) {
for (uint32 x = 0; x < width; ++x) {
uint8 rgb[4] = { 255, 255, 255, 255 };
rgb[r_pos] = ((x / 63) & 1) ? 224 : 32;
rgb[g_pos] = (x % 63 + y % 63) + 96;
rgb[b_pos] = ((y / 63) & 1) ? 224 : 32;
ms->Write(rgb, bytes, NULL, NULL);
}
}
return ms.release();
}
// Convert RGB to 420.
// A negative height inverts the image.
bool ConvertRgb(const talk_base::MemoryStream* ms,
uint32 fourcc, int32 width, int32 height,
WebRtcVideoFrame* frame) {
int r_pos, g_pos, b_pos, bytes;
if (!GetRgbPacking(fourcc, &r_pos, &g_pos, &b_pos, &bytes)) {
return false;
}
int stride = width * bytes;
const uint8* start = reinterpret_cast<const uint8*>(ms->GetBuffer());
if (height < 0) {
height = -height;
start = start + stride * (height - 1);
stride = -stride;
}
frame->InitToBlack(width, height, 0, 0);
for (int32 y = 0; y < height; y += 2) {
for (int32 x = 0; x < width; x += 2) {
const uint8* rgb[4];
uint8 yuv[4][3];
rgb[0] = start + y * stride + x * bytes;
rgb[1] = rgb[0] + bytes;
rgb[2] = rgb[0] + stride;
rgb[3] = rgb[2] + bytes;
for (size_t i = 0; i < 4; ++i) {
ConvertRgbPixel(rgb[i][r_pos], rgb[i][g_pos], rgb[i][b_pos],
&yuv[i][0], &yuv[i][1], &yuv[i][2]);
}
frame->GetYPlane()[width * y + x] = yuv[0][0];
frame->GetYPlane()[width * y + x + 1] = yuv[1][0];
frame->GetYPlane()[width * (y + 1) + x] = yuv[2][0];
frame->GetYPlane()[width * (y + 1) + x + 1] = yuv[3][0];
frame->GetUPlane()[width / 2 * (y / 2) + x / 2] =
(yuv[0][1] + yuv[1][1] + yuv[2][1] + yuv[3][1] + 2) / 4;
frame->GetVPlane()[width / 2 * (y / 2) + x / 2] =
(yuv[0][2] + yuv[1][2] + yuv[2][2] + yuv[3][2] + 2) / 4;
}
}
return true;
}
// Simple and slow RGB->YUV conversion. From NTSC standard, c/o Wikipedia.
void ConvertRgbPixel(uint8 r, uint8 g, uint8 b,
uint8* y, uint8* u, uint8* v) {
*y = static_cast<int>(.257 * r + .504 * g + .098 * b) + 16;
*u = static_cast<int>(-.148 * r - .291 * g + .439 * b) + 128;
*v = static_cast<int>(.439 * r - .368 * g - .071 * b) + 128;
}
bool GetRgbPacking(uint32 fourcc,
int* r_pos, int* g_pos, int* b_pos, int* bytes) {
if (fourcc == cricket::FOURCC_RAW) {
*r_pos = 0;
*g_pos = 1;
*b_pos = 2;
*bytes = 3; // RGB in memory
} else if (fourcc == cricket::FOURCC_24BG) {
*r_pos = 2;
*g_pos = 1;
*b_pos = 0;
*bytes = 3; // BGR in memory
} else if (fourcc == cricket::FOURCC_ABGR) {
*r_pos = 0;
*g_pos = 1;
*b_pos = 2;
*bytes = 4; // RGBA in memory
} else if (fourcc == cricket::FOURCC_BGRA) {
*r_pos = 1;
*g_pos = 2;
*b_pos = 3;
*bytes = 4; // ARGB in memory
} else if (fourcc == cricket::FOURCC_ARGB) {
*r_pos = 2;
*g_pos = 1;
*b_pos = 0;
*bytes = 4; // BGRA in memory
} else {
return false;
}
return true;
}
// Comparison functions for testing.
static bool IsNull(const WebRtcVideoFrame& frame) {
return !frame.HasImage();
}
static bool IsSize(const WebRtcVideoFrame& frame,
uint32 width, uint32 height) {
return frame.HasImage() &&
frame.GetYPitch() >= static_cast<int32>(width) &&
frame.GetUPitch() >= static_cast<int32>(width) / 2 &&
frame.GetVPitch() >= static_cast<int32>(width) / 2 &&
frame.GetWidth() == width && frame.GetHeight() == height;
}
static bool IsPlaneEqual(const std::string& name,
const uint8* plane1, uint32 pitch1,
const uint8* plane2, uint32 pitch2,
uint32 width, uint32 height,
int max_error) {
const uint8* r1 = plane1;
const uint8* r2 = plane2;
for (uint32 y = 0; y < height; ++y) {
for (uint32 x = 0; x < width; ++x) {
if (abs(static_cast<int>(r1[x] - r2[x])) > max_error) {
LOG(LS_INFO) << "IsPlaneEqual(" << name << "): pixel["
<< x << "," << y << "] differs: "
<< static_cast<int>(r1[x]) << " vs "
<< static_cast<int>(r2[x]);
return false;
}
}
r1 += pitch1;
r2 += pitch2;
}
return true;
}
static bool IsFrameContiguous(const WebRtcVideoFrame& frame) {
int width = frame.GetWidth();
int height = frame.GetHeight();
const uint8* y = frame.GetYPlane();
const uint8* u = frame.GetUPlane();
const uint8* v = frame.GetVPlane();
int size = width * height * 3 / 2;
bool u_near = (u - y) < size;
bool v_near = (v - y) < size;
return u_near && v_near;
}
static bool IsEqual(const WebRtcVideoFrame& frame,
size_t width, size_t height,
size_t pixel_width, size_t pixel_height,
int64 elapsed_time, int64 time_stamp,
const uint8* y, uint32 ypitch,
const uint8* u, uint32 upitch,
const uint8* v, uint32 vpitch,
int max_error) {
if (!IsFrameContiguous(frame)) {
LOG(LS_INFO) << "lmi frame is not contiguous";
}
return IsSize(frame, width, height) &&
frame.GetPixelWidth() == pixel_width &&
frame.GetPixelHeight() == pixel_height &&
frame.GetElapsedTime() == elapsed_time &&
frame.GetTimeStamp() == time_stamp &&
IsPlaneEqual("y", frame.GetYPlane(), frame.GetYPitch(), y, ypitch,
width, height, max_error) &&
IsPlaneEqual("u", frame.GetUPlane(), frame.GetUPitch(), u, upitch,
width / 2, height / 2, max_error) &&
IsPlaneEqual("v", frame.GetVPlane(), frame.GetVPitch(), v, vpitch,
width / 2, height / 2, max_error);
}
static bool IsEqual(const WebRtcVideoFrame& frame1,
const WebRtcVideoFrame& frame2,
int max_error) {
return IsEqual(frame1, frame2.GetWidth(), frame2.GetHeight(),
frame2.GetPixelWidth(), frame2.GetPixelHeight(),
frame2.GetElapsedTime(), frame2.GetTimeStamp(),
frame2.GetYPlane(), frame2.GetYPitch(),
frame2.GetUPlane(), frame2.GetUPitch(),
frame2.GetVPlane(), frame2.GetVPitch(),
max_error);
}
protected:
int repeat_;
};
TEST_F(WebRtcVideoFrameTest, ConvertToARGBBuffer) {
size_t out_size = kWidth * kHeight * 4;
talk_base::scoped_array<uint8> outbuf(new uint8[out_size + kAlignment]);
uint8 *out = ALIGNP(outbuf.get(), kAlignment);
WebRtcVideoFrame frame;
ASSERT_TRUE(LoadFrame(kImageFilename, FOURCC_I420, kWidth, kHeight,
&frame, ROTATION_0));
// TODO: Add test to convert these back to I420, to ensure the
// conversion is done correctly.
for (int i = 0; i < repeat_; ++i) {
EXPECT_EQ(out_size, frame.ConvertToRgbBuffer(cricket::FOURCC_ARGB,
out,
out_size, kWidth * 4));
}
}
// Test basic contruction of an image from an I420 buffer.
TEST_F(WebRtcVideoFrameTest, InitI420) {
WebRtcVideoFrame frame;
EXPECT_TRUE(IsNull(frame));
talk_base::scoped_ptr<talk_base::MemoryStream> ms(LoadSample(kImageFilename));
ASSERT_TRUE(ms.get() != NULL);
size_t data_size;
ASSERT_TRUE(ms->GetSize(&data_size));
uint8* buf = reinterpret_cast<uint8*>(ms->GetBuffer());
EXPECT_TRUE(LoadFrame(buf, data_size, FOURCC_I420,
kWidth, kHeight, &frame, ROTATION_0));
const uint8* y = reinterpret_cast<uint8*>(ms->GetBuffer());
const uint8* u = y + kWidth * kHeight;
const uint8* v = u + kWidth * kHeight / 4;
EXPECT_TRUE(IsEqual(frame, kWidth, kHeight, 1, 1, 0, 0,
y, kWidth, u, kWidth / 2, v, kWidth / 2, 0));
}
// Test constructing an image from a I420 buffer
TEST_F(WebRtcVideoFrameTest, ConstructI420) {
WebRtcVideoFrame frame;
talk_base::scoped_ptr<talk_base::MemoryStream> ms(
CreateYuv420Sample(kWidth, kHeight));
EXPECT_TRUE(LoadFrame(ms.get(), cricket::FOURCC_I420,
kWidth, kHeight, &frame, ROTATION_0));
const uint8* y = reinterpret_cast<uint8*>(ms.get()->GetBuffer());
const uint8* u = y + kWidth * kHeight;
const uint8* v = u + kWidth * kHeight / 4;
EXPECT_TRUE(IsEqual(frame, kWidth, kHeight, 1, 1, 0, 0,
y, kWidth, u, kWidth / 2, v, kWidth / 2, 0));
}
// Test creating an empty image and initing it to black.
TEST_F(WebRtcVideoFrameTest, ConstructBlack) {
WebRtcVideoFrame frame;
for (int i = 0; i < repeat_; ++i) {
EXPECT_TRUE(frame.InitToBlack(kWidth, kHeight, 0, 0));
}
EXPECT_TRUE(IsSize(frame, kWidth, kHeight));
EXPECT_EQ(16, *frame.GetYPlane());
EXPECT_EQ(128, *frame.GetUPlane());
EXPECT_EQ(128, *frame.GetVPlane());
}
TEST_F(WebRtcVideoFrameTest, Copy) {
WebRtcVideoFrame frame1;
talk_base::scoped_ptr<cricket::WebRtcVideoFrame> frame2;
ASSERT_TRUE(LoadFrame(kImageFilename, FOURCC_I420, kWidth, kHeight,
&frame1, ROTATION_0));
frame2.reset(static_cast<WebRtcVideoFrame*>(frame1.Copy()));
EXPECT_TRUE(IsEqual(frame1, *frame2.get(), 0));
}
TEST_F(WebRtcVideoFrameTest, CopyToBuffer) {
size_t out_size = kWidth * kHeight * 3 / 2;
talk_base::scoped_array<uint8> out(new uint8[out_size]);
WebRtcVideoFrame frame;
talk_base::scoped_ptr<talk_base::MemoryStream> ms(LoadSample(kImageFilename));
ASSERT_TRUE(ms.get() != NULL);
size_t data_size;
ASSERT_TRUE(ms->GetSize(&data_size));
EXPECT_TRUE(LoadFrame(reinterpret_cast<uint8*>(ms->GetBuffer()),
data_size, FOURCC_I420,
kWidth, kHeight, &frame, ROTATION_0));
for (int i = 0; i < repeat_; ++i) {
EXPECT_EQ(out_size, frame.CopyToBuffer(out.get(), out_size));
}
EXPECT_EQ(0, memcmp(out.get(), ms->GetBuffer(), out_size));
}
TEST_F(WebRtcVideoFrameTest, CopyToBuffer1Pixel) {
size_t out_size = 3;
talk_base::scoped_array<uint8> out(new uint8[out_size + 1]);
memset(out.get(), 0xfb, out_size + 1); // Fill buffer
uint8 pixel[3] = { 1, 2, 3 };
WebRtcVideoFrame frame;
EXPECT_TRUE(LoadFrame(pixel, sizeof(pixel), FOURCC_I420,
1, 1, &frame, ROTATION_0));
for (int i = 0; i < repeat_; ++i) {
EXPECT_EQ(out_size, frame.CopyToBuffer(out.get(), out_size));
}
EXPECT_EQ(1, out.get()[0]); // Check Y. Should be 1.
EXPECT_EQ(2, out.get()[1]); // Check U. Should be 2.
EXPECT_EQ(3, out.get()[2]); // Check V. Should be 3.
EXPECT_EQ(0xfb, out.get()[3]); // Check sentinel is still intact.
}
// TODO: Merge this with the LmiVideoFrame test for more test cases
// when they are supported.