• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.mediav2.common.cts;
18 
19 import static org.junit.Assert.assertTrue;
20 import static org.junit.Assume.assumeFalse;
21 
22 import android.graphics.SurfaceTexture;
23 import android.opengl.EGL14;
24 import android.opengl.EGLConfig;
25 import android.opengl.EGLContext;
26 import android.opengl.EGLDisplay;
27 import android.opengl.EGLSurface;
28 import android.opengl.GLES20;
29 import android.util.Log;
30 import android.view.Surface;
31 
32 
33 /**
34  * Holds state associated with a Surface used for MediaCodec decoder output.
35  * <p>
36  * The (width,height) constructor for this class will prepare GL, create a SurfaceTexture,
37  * and then create a Surface for that SurfaceTexture.  The Surface can be passed to
38  * MediaCodec.configure() to receive decoder output.  When a frame arrives, we latch the
39  * texture with updateTexImage, then render the texture with GL to a pbuffer.
40  * <p>
41  * The no-arg constructor skips the GL preparation step and doesn't allocate a pbuffer.
42  * Instead, it just creates the Surface and SurfaceTexture, and when a frame arrives
43  * we just draw it on whatever surface is current.
44  * <p>
45  * By default, the Surface will be using a BufferQueue in asynchronous mode, so we
46  * can potentially drop frames.
47  */
48 public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener {
49     private static final String TAG = "OutputSurface";
50     private static final boolean VERBOSE = false;
51 
52     private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
53     private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
54     private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
55 
56     private SurfaceTexture mSurfaceTexture;
57     private Surface mSurface;
58 
59     private Object mFrameSyncObject = new Object();     // guards mFrameAvailable
60     private boolean mFrameAvailable;
61 
62     private TextureRender mTextureRender;
63     private int mEGLESVersion;
64     private boolean mEXTYuvTargetSupported = false;
65 
66     /**
67      * Creates an OutputSurface backed by a pbuffer with the specified dimensions.  The new
68      * EGL context and surface will be made current.  Creates a Surface that can be passed
69      * to MediaCodec.configure().
70      */
OutputSurface(int width, int height, boolean useHighBitDepth)71     public OutputSurface(int width, int height, boolean useHighBitDepth) {
72         this(width, height, useHighBitDepth, /* useYuvSampling */ false);
73     }
74 
OutputSurface(int width, int height, boolean useHighBitDepth, boolean useYuvSampling)75     public OutputSurface(int width, int height, boolean useHighBitDepth, boolean useYuvSampling) {
76         if (width <= 0 || height <= 0) {
77             throw new IllegalArgumentException();
78         }
79 
80         eglSetup(width, height, useHighBitDepth, useYuvSampling);
81         makeCurrent();
82 
83         if (mEGLESVersion > 2) {
84             String extensionList = GLES20.glGetString(GLES20.GL_EXTENSIONS);
85             mEXTYuvTargetSupported = extensionList.contains("GL_EXT_YUV_target");
86         }
87 
88         setup(this, useYuvSampling);
89     }
90 
91     /**
92      * Creates an OutputSurface using the current EGL context (rather than establishing a
93      * new one).  Creates a Surface that can be passed to MediaCodec.configure().
94      */
OutputSurface()95     public OutputSurface() {
96         setup(this, /* useYuvSampling */ false);
97     }
98 
OutputSurface(final SurfaceTexture.OnFrameAvailableListener listener)99     public OutputSurface(final SurfaceTexture.OnFrameAvailableListener listener) {
100         setup(listener, /* useYuvSampling */ false);
101     }
102 
103     /**
104      * Returns if the device support GL_EXT_YUV_target extension
105      */
getEXTYuvTargetSupported()106     public boolean getEXTYuvTargetSupported() {
107         return mEXTYuvTargetSupported;
108     }
109 
110     /**
111      * Creates instances of TextureRender and SurfaceTexture, and a Surface associated
112      * with the SurfaceTexture.
113      */
setup(SurfaceTexture.OnFrameAvailableListener listener, boolean useYuvSampling)114     private void setup(SurfaceTexture.OnFrameAvailableListener listener, boolean useYuvSampling) {
115         assertTrue(EGL14.eglGetCurrentContext() != EGL14.EGL_NO_CONTEXT);
116         assertTrue(EGL14.eglGetCurrentDisplay() != EGL14.EGL_NO_DISPLAY);
117         assertTrue(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW) != EGL14.EGL_NO_SURFACE);
118         assertTrue(EGL14.eglGetCurrentSurface(EGL14.EGL_READ) != EGL14.EGL_NO_SURFACE);
119         mTextureRender = new TextureRender();
120         mTextureRender.setUseYuvSampling(mEXTYuvTargetSupported && useYuvSampling);
121         mTextureRender.surfaceCreated();
122 
123         // Even if we don't access the SurfaceTexture after the constructor returns, we
124         // still need to keep a reference to it.  The Surface doesn't retain a reference
125         // at the Java level, so if we don't either then the object can get GCed, which
126         // causes the native finalizer to run.
127         if (VERBOSE) Log.d(TAG, "textureID=" + mTextureRender.getTextureId());
128         mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId());
129 
130         // This doesn't work if OutputSurface is created on the thread that CTS started for
131         // these test cases.
132         //
133         // The CTS-created thread has a Looper, and the SurfaceTexture constructor will
134         // create a Handler that uses it.  The "frame available" message is delivered
135         // there, but since we're not a Looper-based thread we'll never see it.  For
136         // this to do anything useful, OutputSurface must be created on a thread without
137         // a Looper, so that SurfaceTexture uses the main application Looper instead.
138         //
139         // Java language note: passing "this" out of a constructor is generally unwise,
140         // but we should be able to get away with it here.
141         mSurfaceTexture.setOnFrameAvailableListener(listener);
142 
143         mSurface = new Surface(mSurfaceTexture);
144     }
145 
146     /**
147      * Prepares EGL.  We want a GLES 2.0 context and a surface that supports pbuffer.
148      */
eglSetup(int width, int height, boolean useHighBitDepth, boolean useYuvSampling)149     private void eglSetup(int width, int height, boolean useHighBitDepth, boolean useYuvSampling) {
150         mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
151         if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
152             throw new RuntimeException("unable to get EGL14 display");
153         }
154         int[] version = new int[2];
155         if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
156             mEGLDisplay = null;
157             throw new RuntimeException("unable to initialize EGL14");
158         }
159 
160         // Configure EGL for pbuffer and OpenGL ES 2.0.  We want enough RGB bits
161         // to be able to tell if the frame is reasonable.
162         int eglColorSize = useHighBitDepth ? 10 : 8;
163         int eglAlphaSize = useHighBitDepth ? 2 : 0;
164         int[] attribList = {
165                 EGL14.EGL_RED_SIZE, eglColorSize,
166                 EGL14.EGL_GREEN_SIZE, eglColorSize,
167                 EGL14.EGL_BLUE_SIZE, eglColorSize,
168                 EGL14.EGL_ALPHA_SIZE, eglAlphaSize,
169                 EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
170                 EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT,
171                 EGL14.EGL_NONE
172         };
173         EGLConfig[] configs = new EGLConfig[1];
174         int[] numConfigs = new int[1];
175         if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
176                 numConfigs, 0) || numConfigs[0] == 0) {
177             String message = "Unable to find EGL config supporting renderable-type:ES2 "
178                     + "surface-type:pbuffer r:" + eglColorSize + " g:" + eglColorSize
179                     + " b:" + eglColorSize + " a:" + eglAlphaSize + ".";
180             // When eglChooseConfig fails for RGBA10102, skip high bit depth testing as it is not
181             // mandatory for devices to support this configuration.
182             assumeFalse(message + " Skipping the test for high bit depth case", useHighBitDepth);
183             throw new RuntimeException(message);
184         }
185 
186         // Configure context for OpenGL ES 3.0/2.0.
187         mEGLESVersion = useYuvSampling ? 3 : 2;
188         do {
189             int[] attrib_list = {
190                     EGL14.EGL_CONTEXT_CLIENT_VERSION, mEGLESVersion,
191                     EGL14.EGL_NONE
192             };
193             mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT,
194                     attrib_list, 0);
195             // if OpenGL ES 3.0 isn't supported, attempt to create OpenGL ES 2.0 context
196             if (mEGLContext == EGL14.EGL_NO_CONTEXT && useYuvSampling) {
197                 mEGLESVersion--;
198             } else {
199                 break;
200             }
201         } while (mEGLESVersion > 1);
202 
203         checkEglError("eglCreateContext");
204         if (mEGLContext == null) {
205             throw new RuntimeException("null context");
206         }
207 
208         // Create a pbuffer surface.  By using this for output, we can use glReadPixels
209         // to test values in the output.
210         int[] surfaceAttribs = {
211                 EGL14.EGL_WIDTH, width,
212                 EGL14.EGL_HEIGHT, height,
213                 EGL14.EGL_NONE
214         };
215         mEGLSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs, 0);
216         checkEglError("eglCreatePbufferSurface");
217         if (mEGLSurface == null) {
218             throw new RuntimeException("surface was null");
219         }
220     }
221 
222     /**
223      * Discard all resources held by this class, notably the EGL context.
224      */
release()225     public void release() {
226         if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
227             EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
228             EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
229             EGL14.eglReleaseThread();
230             EGL14.eglTerminate(mEGLDisplay);
231         }
232 
233         mSurface.release();
234 
235         mSurfaceTexture.release();
236 
237         mEGLDisplay = EGL14.EGL_NO_DISPLAY;
238         mEGLContext = EGL14.EGL_NO_CONTEXT;
239         mEGLSurface = EGL14.EGL_NO_SURFACE;
240 
241         mTextureRender = null;
242         mSurface = null;
243         mSurfaceTexture = null;
244     }
245 
246     /**
247      * Makes our EGL context and surface current.
248      */
makeCurrent()249     public void makeCurrent() {
250         if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
251             throw new RuntimeException("eglMakeCurrent failed");
252         }
253     }
254 
255     /**
256      * Returns the Surface that we draw onto.
257      */
getSurface()258     public Surface getSurface() {
259         return mSurface;
260     }
261 
262     /**
263      * Replaces the fragment shader.
264      */
changeFragmentShader(String fragmentShader)265     public void changeFragmentShader(String fragmentShader) {
266         mTextureRender.changeFragmentShader(fragmentShader);
267     }
268 
269     /**
270      * Latches the next buffer into the texture.  Must be called from the thread that created
271      * the OutputSurface object, after the onFrameAvailable callback has signaled that new
272      * data is available.
273      */
awaitNewImage()274     public void awaitNewImage() {
275         final int timeOutMS = 2000;
276 
277         synchronized (mFrameSyncObject) {
278             while (!mFrameAvailable) {
279                 try {
280                     // Wait for onFrameAvailable() to signal us.  Use a timeout to avoid
281                     // stalling the test if it doesn't arrive.
282                     mFrameSyncObject.wait(timeOutMS);
283                     if (!mFrameAvailable) {
284                         // TODO: if "spurious wakeup", continue while loop
285                         throw new RuntimeException("Surface frame wait timed out");
286                     }
287                 } catch (InterruptedException ie) {
288                     // shouldn't happen
289                     throw new RuntimeException(ie);
290                 }
291             }
292             mFrameAvailable = false;
293         }
294 
295         // Latch the data.
296         mTextureRender.checkGlError("before updateTexImage");
297         mSurfaceTexture.updateTexImage();
298     }
299 
300     /**
301      * Wait for new image to become available or until timeout, whichever comes first.
302      * @param timeoutMs
303      * @return true if new image is available. false for no new image until timeout.
304      */
checkForNewImage(int timeoutMs)305     public boolean checkForNewImage(int timeoutMs) {
306         synchronized (mFrameSyncObject) {
307             while (!mFrameAvailable) {
308                 try {
309                     // Wait for onFrameAvailable() to signal us.  Use a timeout to avoid
310                     // stalling the test if it doesn't arrive.
311                     mFrameSyncObject.wait(timeoutMs);
312                     if (!mFrameAvailable) {
313                         return false;
314                     }
315                 } catch (InterruptedException ie) {
316                     // shouldn't happen
317                     throw new RuntimeException(ie);
318                 }
319             }
320             mFrameAvailable = false;
321         }
322 
323         // Latch the data.
324         mTextureRender.checkGlError("before updateTexImage");
325         mSurfaceTexture.updateTexImage();
326         return true;
327     }
328 
329     /**
330      * Draws the data from SurfaceTexture onto the current EGL surface.
331      */
drawImage()332     public void drawImage() {
333         mTextureRender.drawFrame(mSurfaceTexture);
334     }
335 
latchImage()336     public void latchImage() {
337         mTextureRender.checkGlError("before updateTexImage");
338         mSurfaceTexture.updateTexImage();
339     }
340 
341     @Override
onFrameAvailable(SurfaceTexture st)342     public void onFrameAvailable(SurfaceTexture st) {
343         if (VERBOSE) Log.d(TAG, "new frame available");
344         synchronized (mFrameSyncObject) {
345             if (mFrameAvailable) {
346                 throw new RuntimeException("mFrameAvailable already set, frame could be dropped");
347             }
348             mFrameAvailable = true;
349             mFrameSyncObject.notifyAll();
350         }
351     }
352 
353     /**
354      * Checks for EGL errors.
355      */
checkEglError(String msg)356     private void checkEglError(String msg) {
357         int error = EGL14.eglGetError();
358         if (error != EGL14.EGL_SUCCESS) {
359             throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
360         }
361     }
362 }
363