blob: 4f8b19927091a77d005f4b8e6448950748cc6ff6 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/gpu/test/video_player/frame_renderer_thumbnail.h"
#include <utility>
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "media/gpu/test/video_test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_surface_egl.h"
#include "ui/gl/init/gl_factory.h"
namespace media {
namespace test {
namespace {
// Size of the large image to which the thumbnails will be rendered.
constexpr gfx::Size kThumbnailsPageSize(1600, 1200);
// Size of the individual thumbnails that will be rendered.
constexpr gfx::Size kThumbnailSize(160, 120);
// Default filename used to store the thumbnails image.
constexpr const base::FilePath::CharType* kThumbnailFilename =
FILE_PATH_LITERAL("thumbnail.png");
// Vertex shader used to render thumbnails.
constexpr char kVertexShader[] =
"varying vec2 interp_tc;\n"
"attribute vec4 in_pos;\n"
"attribute vec2 in_tc;\n"
"uniform bool tex_flip; void main() {\n"
" if (tex_flip)\n"
" interp_tc = vec2(in_tc.x, 1.0 - in_tc.y);\n"
" else\n"
" interp_tc = in_tc;\n"
" gl_Position = in_pos;\n"
"}\n";
// Fragment shader used to render thumbnails.
#if !defined(OS_WIN)
constexpr char kFragmentShader[] =
"#extension GL_OES_EGL_image_external : enable\n"
"precision mediump float;\n"
"varying vec2 interp_tc;\n"
"uniform sampler2D tex;\n"
"#ifdef GL_OES_EGL_image_external\n"
"uniform samplerExternalOES tex_external;\n"
"#endif\n"
"void main() {\n"
" vec4 color = texture2D(tex, interp_tc);\n"
"#ifdef GL_OES_EGL_image_external\n"
" color += texture2D(tex_external, interp_tc);\n"
"#endif\n"
" gl_FragColor = color;\n"
"}\n";
#else
constexpr char kFragmentShader[] =
"#ifdef GL_ES\n"
"precision mediump float;\n"
"#endif\n"
"varying vec2 interp_tc;\n"
"uniform sampler2D tex;\n"
"void main() {\n"
" gl_FragColor = texture2D(tex, interp_tc);\n"
"}\n";
#endif
GLuint CreateTexture(GLenum texture_target, const gfx::Size& size) {
GLuint texture_id;
glGenTextures(1, &texture_id);
glBindTexture(texture_target, texture_id);
if (texture_target == GL_TEXTURE_2D) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.width(), size.height(), 0,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
}
glTexParameteri(texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// OpenGLES2.0.25 section 3.8.2 requires CLAMP_TO_EDGE for NPOT textures.
glTexParameteri(texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
return texture_id;
}
void DeleteTexture(uint32_t texture_id) {
glDeleteTextures(1, &texture_id);
CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
}
void RenderTexture(uint32_t texture_target, uint32_t texture_id) {
// The ExternalOES sampler is bound to GL_TEXTURE1 and the Texture2D sampler
// is bound to GL_TEXTURE0.
if (texture_target == GL_TEXTURE_2D) {
glActiveTexture(GL_TEXTURE0 + 0);
} else if (texture_target == GL_TEXTURE_EXTERNAL_OES) {
glActiveTexture(GL_TEXTURE0 + 1);
}
glBindTexture(texture_target, texture_id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindTexture(texture_target, 0);
CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
}
void CreateShader(GLuint program, GLenum type, const char* source, int size) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, &size);
glCompileShader(shader);
int result = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
if (!result) {
char log[4096];
glGetShaderInfoLog(shader, base::size(log), nullptr, log);
LOG(FATAL) << log;
}
glAttachShader(program, shader);
glDeleteShader(shader);
CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
}
void GLSetViewPort(const gfx::Rect& area) {
glViewport(area.x(), area.y(), area.width(), area.height());
glScissor(area.x(), area.y(), area.width(), area.height());
}
// Helper function to convert from RGBA to RGB. Returns false if any alpha
// channel is not 0xff, otherwise true.
bool ConvertRGBAToRGB(const std::vector<unsigned char>& rgba,
std::vector<unsigned char>* rgb) {
size_t num_pixels = rgba.size() / 4;
rgb->resize(num_pixels * 3);
// Drop the alpha channel, but check as we go that it is all 0xff.
bool solid = true;
for (size_t i = 0; i < num_pixels; i++) {
(*rgb)[3 * i] = rgba[4 * i];
(*rgb)[3 * i + 1] = rgba[4 * i + 1];
(*rgb)[3 * i + 2] = rgba[4 * i + 2];
solid = solid && (rgba[4 * i + 3] == 0xff);
}
return solid;
}
} // namespace
bool FrameRendererThumbnail::gl_initialized_ = false;
FrameRendererThumbnail::FrameRendererThumbnail(
const std::vector<std::string>& thumbnail_checksums,
const base::FilePath& output_folder)
: thumbnail_checksums_(thumbnail_checksums), output_folder_(output_folder) {
DETACH_FROM_SEQUENCE(renderer_sequence_checker_);
}
FrameRendererThumbnail::~FrameRendererThumbnail() {
DCHECK_CALLED_ON_VALID_SEQUENCE(client_sequence_checker_);
if (renderer_task_runner_) {
base::WaitableEvent done;
renderer_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&FrameRendererThumbnail::DestroyTask,
base::Unretained(this), &done));
done.Wait();
}
}
// static
std::unique_ptr<FrameRendererThumbnail> FrameRendererThumbnail::Create(
const std::vector<std::string> thumbnail_checksums,
const base::FilePath& output_folder) {
auto frame_renderer = base::WrapUnique(
new FrameRendererThumbnail(thumbnail_checksums, output_folder));
frame_renderer->Initialize();
return frame_renderer;
}
bool FrameRendererThumbnail::AcquireGLContext() {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
return gl_context_->MakeCurrent(gl_surface_.get());
}
gl::GLContext* FrameRendererThumbnail::GetGLContext() {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
return gl_context_.get();
}
void FrameRendererThumbnail::RenderFrame(
scoped_refptr<VideoFrame> video_frame) {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
if (video_frame->metadata().end_of_stream)
return;
if (!renderer_task_runner_)
renderer_task_runner_ = base::ThreadTaskRunnerHandle::Get();
if (thumbnails_texture_id_ == 0u)
InitializeThumbnailImageTask();
if (video_frame->visible_rect().size().IsEmpty()) {
// This occurs in bitstream buffer in webrtc scenario.
DLOG(WARNING) << "Skipping rendering, because visible_rect is empty";
return;
}
// Find the texture associated with the video frame's mailbox.
const gpu::MailboxHolder& mailbox_holder = video_frame->mailbox_holder(0);
const gpu::Mailbox& mailbox = mailbox_holder.mailbox;
auto it = mailbox_texture_map_.find(mailbox);
ASSERT_NE(it, mailbox_texture_map_.end());
RenderThumbnailTask(mailbox_holder.texture_target, it->second);
}
void FrameRendererThumbnail::WaitUntilRenderingDone() {}
scoped_refptr<VideoFrame> FrameRendererThumbnail::CreateVideoFrame(
VideoPixelFormat pixel_format,
const gfx::Size& texture_size,
uint32_t texture_target,
uint32_t* texture_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
// Make the GL context current in the case it's not currently yet.
AcquireGLContext();
// Create a mailbox.
gpu::Mailbox mailbox = gpu::Mailbox::Generate();
gpu::MailboxHolder mailbox_holders[media::VideoFrame::kMaxPlanes];
mailbox_holders[0] =
gpu::MailboxHolder(mailbox, gpu::SyncToken(), texture_target);
// Create a new video frame associated with the mailbox.
base::OnceCallback<void(const gpu::SyncToken&)> mailbox_holder_release_cb =
base::BindOnce(&FrameRendererThumbnail::DeleteTextureTask,
base::Unretained(this), mailbox);
scoped_refptr<VideoFrame> frame = VideoFrame::WrapNativeTextures(
pixel_format, mailbox_holders, std::move(mailbox_holder_release_cb),
texture_size, gfx::Rect(texture_size), texture_size, base::TimeDelta());
// Create a texture and associate it with the mailbox.
*texture_id = CreateTexture(texture_target, texture_size);
mailbox_texture_map_.insert(std::make_pair(mailbox, *texture_id));
return frame;
}
bool FrameRendererThumbnail::ValidateThumbnail() {
DCHECK_CALLED_ON_VALID_SEQUENCE(client_sequence_checker_);
if (!renderer_task_runner_)
return false;
bool success = false;
base::WaitableEvent done;
renderer_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&FrameRendererThumbnail::ValidateThumbnailTask,
base::Unretained(this), &success, &done));
done.Wait();
return success;
}
void FrameRendererThumbnail::SaveThumbnailTask() {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
// Create the directory tree if it doesn't exist yet.
if (!DirectoryExists(output_folder_))
base::CreateDirectory(output_folder_);
const std::vector<uint8_t> rgba = ConvertThumbnailToRGBATask();
// Convert raw RGBA into PNG for export.
std::vector<unsigned char> png;
gfx::PNGCodec::Encode(&rgba[0], gfx::PNGCodec::FORMAT_RGBA,
kThumbnailsPageSize, kThumbnailsPageSize.width() * 4,
true, std::vector<gfx::PNGCodec::Comment>(), &png);
base::FilePath filepath =
base::MakeAbsoluteFilePath(output_folder_).Append(kThumbnailFilename);
LOG(INFO) << "Saving thumbnails image to " << filepath;
base::File thumbnail_file(
filepath, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
int num_bytes =
thumbnail_file.Write(0u, reinterpret_cast<char*>(&png[0]), png.size());
ASSERT_NE(-1, num_bytes);
EXPECT_EQ(static_cast<size_t>(num_bytes), png.size());
}
void FrameRendererThumbnail::Initialize() {
DCHECK_CALLED_ON_VALID_SEQUENCE(client_sequence_checker_);
// Initialize GL rendering and create GL context.
if (!gl_initialized_) {
if (!gl::init::InitializeGLOneOff())
LOG(FATAL) << "Could not initialize GL";
gl_initialized_ = true;
}
gl_surface_ = gl::init::CreateOffscreenGLSurface(gfx::Size());
gl_context_ = gl::init::CreateGLContext(nullptr, gl_surface_.get(),
gl::GLContextAttribs());
}
void FrameRendererThumbnail::DestroyTask(base::WaitableEvent* done) {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
DCHECK(mailbox_texture_map_.empty());
DestroyThumbnailImageTask();
// Release the |gl_context_| so it can be destroyed on the same thread it was
// created on. Otherwise random crashes might occur as not all resources are
// freed correctly.
gl_context_->ReleaseCurrent(gl_surface_.get());
done->Signal();
}
void FrameRendererThumbnail::InitializeThumbnailImageTask() {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
GLint max_texture_size;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
CHECK_GE(max_texture_size, kThumbnailsPageSize.width());
CHECK_GE(max_texture_size, kThumbnailsPageSize.height());
thumbnails_fbo_size_ = kThumbnailsPageSize;
thumbnail_size_ = kThumbnailSize;
glGenFramebuffersEXT(1, &thumbnails_fbo_id_);
glGenTextures(1, &thumbnails_texture_id_);
glBindTexture(GL_TEXTURE_2D, thumbnails_texture_id_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, thumbnails_fbo_size_.width(),
thumbnails_fbo_size_.height(), 0, GL_RGB,
GL_UNSIGNED_SHORT_5_6_5, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
thumbnails_texture_id_, 0);
GLenum fb_status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER);
CHECK(fb_status == GL_FRAMEBUFFER_COMPLETE) << fb_status;
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebufferEXT(GL_FRAMEBUFFER,
gl_surface_->GetBackingFramebufferObject());
// These vertices and texture coords map (0,0) in the texture to the bottom
// left of the viewport. Since we get the video frames with the the top left
// at (0,0) we need to flip the texture y coordinate in the vertex shader for
// this to be rendered the right way up. In the case of thumbnail rendering we
// use the same vertex shader to render the FBO to the screen, where we do not
// want this flipping. Vertices are 2 floats for position and 2 floats for
// texcoord each.
const float kVertices[] = {
-1, 1, 0, 1, // Vertex 0
-1, -1, 0, 0, // Vertex 1
1, 1, 1, 1, // Vertex 2
1, -1, 1, 0, // Vertex 3
};
const GLvoid* kVertexPositionOffset = 0;
const GLvoid* kVertexTexcoordOffset =
reinterpret_cast<GLvoid*>(sizeof(float) * 2);
const GLsizei kVertexStride = sizeof(float) * 4;
glGenBuffersARB(1, &vertex_buffer_);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
program_ = glCreateProgram();
CreateShader(program_, GL_VERTEX_SHADER, kVertexShader,
base::size(kVertexShader));
CreateShader(program_, GL_FRAGMENT_SHADER, kFragmentShader,
base::size(kFragmentShader));
glLinkProgram(program_);
GLint result = GL_FALSE;
glGetProgramiv(program_, GL_LINK_STATUS, &result);
if (!result) {
constexpr GLsizei kLogBufferSize = 4096;
char log[kLogBufferSize];
glGetShaderInfoLog(program_, kLogBufferSize, nullptr, log);
LOG(FATAL) << log;
}
glUseProgram(program_);
glDeleteProgram(program_);
glUniform1i(glGetUniformLocation(program_, "tex_flip"), 0);
glUniform1i(glGetUniformLocation(program_, "tex"), 0);
GLint tex_external = glGetUniformLocation(program_, "tex_external");
if (tex_external != -1) {
glUniform1i(tex_external, 1);
}
GLint pos_location = glGetAttribLocation(program_, "in_pos");
glEnableVertexAttribArray(pos_location);
glVertexAttribPointer(pos_location, 2, GL_FLOAT, GL_FALSE, kVertexStride,
kVertexPositionOffset);
GLint tc_location = glGetAttribLocation(program_, "in_tc");
glEnableVertexAttribArray(tc_location);
glVertexAttribPointer(tc_location, 2, GL_FLOAT, GL_FALSE, kVertexStride,
kVertexTexcoordOffset);
// Unbind the vertex buffer
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void FrameRendererThumbnail::DestroyThumbnailImageTask() {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
glDeleteTextures(1, &thumbnails_texture_id_);
glDeleteFramebuffersEXT(1, &thumbnails_fbo_id_);
glDeleteBuffersARB(1, &vertex_buffer_);
thumbnails_texture_id_ = 0u;
thumbnails_fbo_id_ = 0u;
vertex_buffer_ = 0u;
}
void FrameRendererThumbnail::RenderThumbnailTask(uint32_t texture_target,
uint32_t texture_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
const int width = thumbnail_size_.width();
const int height = thumbnail_size_.height();
const int thumbnails_in_row = thumbnails_fbo_size_.width() / width;
const int thumbnails_in_column = thumbnails_fbo_size_.height() / height;
const int row = (frame_count_ / thumbnails_in_row) % thumbnails_in_column;
const int col = frame_count_ % thumbnails_in_row;
gfx::Rect area(col * width, row * height, width, height);
glUniform1i(glGetUniformLocation(program_, "tex_flip"), 0);
glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
GLSetViewPort(area);
RenderTexture(texture_target, texture_id);
glBindFramebufferEXT(GL_FRAMEBUFFER,
gl_surface_->GetBackingFramebufferObject());
// We need to flush the GL commands before returning the thumbnail texture to
// the decoder.
glFlush();
++frame_count_;
}
const std::vector<uint8_t>
FrameRendererThumbnail::ConvertThumbnailToRGBATask() {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
std::vector<uint8_t> rgba;
const size_t num_pixels = thumbnails_fbo_size_.GetArea();
rgba.resize(num_pixels * 4);
glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
// We can only count on GL_RGBA/GL_UNSIGNED_BYTE support.
glReadPixels(0, 0, thumbnails_fbo_size_.width(),
thumbnails_fbo_size_.height(), GL_RGBA, GL_UNSIGNED_BYTE,
&(rgba)[0]);
glBindFramebufferEXT(GL_FRAMEBUFFER,
gl_surface_->GetBackingFramebufferObject());
return rgba;
}
void FrameRendererThumbnail::ValidateThumbnailTask(bool* success,
base::WaitableEvent* done) {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
const std::vector<uint8_t> rgba = ConvertThumbnailToRGBATask();
// Convert the thumbnail from RGBA to RGB.
std::vector<uint8_t> rgb;
EXPECT_EQ(ConvertRGBAToRGB(rgba, &rgb), true)
<< "RGBA frame has incorrect alpha";
// Calculate the thumbnail's checksum and compare it to golden values.
std::string md5_string = base::MD5String(
base::StringPiece(reinterpret_cast<char*>(&rgb[0]), rgb.size()));
*success = base::Contains(thumbnail_checksums_, md5_string);
// If validation failed, write the thumbnail image to disk.
if (!(*success))
SaveThumbnailTask();
done->Signal();
}
void FrameRendererThumbnail::DeleteTextureTask(const gpu::Mailbox& mailbox,
const gpu::SyncToken&) {
DCHECK_CALLED_ON_VALID_SEQUENCE(renderer_sequence_checker_);
auto it = mailbox_texture_map_.find(mailbox);
ASSERT_NE(it, mailbox_texture_map_.end());
uint32_t texture_id = it->second;
mailbox_texture_map_.erase(mailbox);
DeleteTexture(texture_id);
}
} // namespace test
} // namespace media