/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.filterpacks.ui;
import android.filterfw.core.Filter;
import android.filterfw.core.FilterContext;
import android.filterfw.core.FilterSurfaceView;
import android.filterfw.core.Frame;
import android.filterfw.core.FrameFormat;
import android.filterfw.core.GenerateFieldPort;
import android.filterfw.core.GenerateFinalPort;
import android.filterfw.core.GLEnvironment;
import android.filterfw.core.GLFrame;
import android.filterfw.core.MutableFrameFormat;
import android.filterfw.core.ShaderProgram;
import android.filterfw.format.ImageFormat;
import android.view.SurfaceHolder;
import android.util.Log;
/**
* @hide
*/
public class SurfaceRenderFilter extends Filter implements SurfaceHolder.Callback {
private final int RENDERMODE_STRETCH = 0;
private final int RENDERMODE_FIT = 1;
private final int RENDERMODE_FILL_CROP = 2;
/** Required. Sets the destination filter surface view for this
* node.
*/
@GenerateFinalPort(name = "surfaceView")
private FilterSurfaceView mSurfaceView;
/** Optional. Control how the incoming frames are rendered onto the
* output. Default is FIT.
* RENDERMODE_STRETCH: Just fill the output surfaceView.
* RENDERMODE_FIT: Keep aspect ratio and fit without cropping. May
* have black bars.
* RENDERMODE_FILL_CROP: Keep aspect ratio and fit without black
* bars. May crop.
*/
@GenerateFieldPort(name = "renderMode", hasDefault = true)
private String mRenderModeString;
private boolean mIsBound = false;
private ShaderProgram mProgram;
private GLFrame mScreen;
private int mRenderMode = RENDERMODE_FIT;
private float mAspectRatio = 1.f;
private int mScreenWidth;
private int mScreenHeight;
private boolean mLogVerbose;
private static final String TAG = "SurfaceRenderFilter";
public SurfaceRenderFilter(String name) {
super(name);
mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
}
@Override
public void setupPorts() {
// Make sure we have a SurfaceView
if (mSurfaceView == null) {
throw new RuntimeException("NULL SurfaceView passed to SurfaceRenderFilter");
}
// Add input port
addMaskedInputPort("frame", ImageFormat.create(ImageFormat.COLORSPACE_RGBA));
}
public void updateRenderMode() {
if (mRenderModeString != null) {
if (mRenderModeString.equals("stretch")) {
mRenderMode = RENDERMODE_STRETCH;
} else if (mRenderModeString.equals("fit")) {
mRenderMode = RENDERMODE_FIT;
} else if (mRenderModeString.equals("fill_crop")) {
mRenderMode = RENDERMODE_FILL_CROP;
} else {
throw new RuntimeException("Unknown render mode '" + mRenderModeString + "'!");
}
}
updateTargetRect();
}
@Override
public void prepare(FilterContext context) {
// Create identity shader to render, and make sure to render upside-down, as textures
// are stored internally bottom-to-top.
mProgram = ShaderProgram.createIdentity(context);
mProgram.setSourceRect(0, 1, 1, -1);
mProgram.setClearsOutput(true);
mProgram.setClearColor(0.0f, 0.0f, 0.0f);
updateRenderMode();
// Create a frame representing the screen
MutableFrameFormat screenFormat = ImageFormat.create(mSurfaceView.getWidth(),
mSurfaceView.getHeight(),
ImageFormat.COLORSPACE_RGBA,
FrameFormat.TARGET_GPU);
mScreen = (GLFrame)context.getFrameManager().newBoundFrame(screenFormat,
GLFrame.EXISTING_FBO_BINDING,
0);
}
@Override
public void open(FilterContext context) {
// Bind surface view to us. This will emit a surfaceCreated and surfaceChanged call that
// will update our screen width and height.
mSurfaceView.unbind();
mSurfaceView.bindToListener(this, context.getGLEnvironment());
}
@Override
public void process(FilterContext context) {
// Make sure we are bound to a surface before rendering
if (!mIsBound) {
Log.w("SurfaceRenderFilter",
this + ": Ignoring frame as there is no surface to render to!");
return;
}
if (mLogVerbose) Log.v(TAG, "Starting frame processing");
GLEnvironment glEnv = mSurfaceView.getGLEnv();
if (glEnv != context.getGLEnvironment()) {
throw new RuntimeException("Surface created under different GLEnvironment!");
}
// Get input frame
Frame input = pullInput("frame");
boolean createdFrame = false;
float currentAspectRatio = (float)input.getFormat().getWidth() / input.getFormat().getHeight();
if (currentAspectRatio != mAspectRatio) {
if (mLogVerbose) Log.v(TAG, "New aspect ratio: " + currentAspectRatio +", previously: " + mAspectRatio);
mAspectRatio = currentAspectRatio;
updateTargetRect();
}
// See if we need to copy to GPU
Frame gpuFrame = null;
if (mLogVerbose) Log.v("SurfaceRenderFilter", "Got input format: " + input.getFormat());
int target = input.getFormat().getTarget();
if (target != FrameFormat.TARGET_GPU) {
gpuFrame = context.getFrameManager().duplicateFrameToTarget(input,
FrameFormat.TARGET_GPU);
createdFrame = true;
} else {
gpuFrame = input;
}
// Activate our surface
glEnv.activateSurfaceWithId(mSurfaceView.getSurfaceId());
// Process
mProgram.process(gpuFrame, mScreen);
// And swap buffers
glEnv.swapBuffers();
if (createdFrame) {
gpuFrame.release();
}
}
@Override
public void fieldPortValueUpdated(String name, FilterContext context) {
updateTargetRect();
}
@Override
public void close(FilterContext context) {
mSurfaceView.unbind();
}
@Override
public void tearDown(FilterContext context) {
if (mScreen != null) {
mScreen.release();
}
}
@Override
public synchronized void surfaceCreated(SurfaceHolder holder) {
mIsBound = true;
}
@Override
public synchronized void surfaceChanged(SurfaceHolder holder,
int format,
int width,
int height) {
// If the screen is null, we do not care about surface changes (yet). Once we have a
// screen object, we need to keep track of these changes.
if (mScreen != null) {
mScreenWidth = width;
mScreenHeight = height;
mScreen.setViewport(0, 0, mScreenWidth, mScreenHeight);
updateTargetRect();
}
}
@Override
public synchronized void surfaceDestroyed(SurfaceHolder holder) {
mIsBound = false;
}
private void updateTargetRect() {
if (mScreenWidth > 0 && mScreenHeight > 0 && mProgram != null) {
float screenAspectRatio = (float)mScreenWidth / mScreenHeight;
float relativeAspectRatio = screenAspectRatio / mAspectRatio;
switch (mRenderMode) {
case RENDERMODE_STRETCH:
mProgram.setTargetRect(0, 0, 1, 1);
break;
case RENDERMODE_FIT:
if (relativeAspectRatio > 1.0f) {
// Screen is wider than the camera, scale down X
mProgram.setTargetRect(0.5f - 0.5f / relativeAspectRatio, 0.0f,
1.0f / relativeAspectRatio, 1.0f);
} else {
// Screen is taller than the camera, scale down Y
mProgram.setTargetRect(0.0f, 0.5f - 0.5f * relativeAspectRatio,
1.0f, relativeAspectRatio);
}
break;
case RENDERMODE_FILL_CROP:
if (relativeAspectRatio > 1) {
// Screen is wider than the camera, crop in Y
mProgram.setTargetRect(0.0f, 0.5f - 0.5f * relativeAspectRatio,
1.0f, relativeAspectRatio);
} else {
// Screen is taller than the camera, crop in X
mProgram.setTargetRect(0.5f - 0.5f / relativeAspectRatio, 0.0f,
1.0f / relativeAspectRatio, 1.0f);
}
break;
}
}
}
}