1 /* 2 * Copyright 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 package com.google.android.exoplayer2.transformer; 17 18 import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 19 import static com.google.android.exoplayer2.util.Assertions.checkState; 20 import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; 21 import static com.google.common.collect.Iterables.getLast; 22 23 import android.content.Context; 24 import android.graphics.SurfaceTexture; 25 import android.opengl.EGL14; 26 import android.opengl.EGLContext; 27 import android.opengl.EGLDisplay; 28 import android.opengl.EGLExt; 29 import android.opengl.EGLSurface; 30 import android.opengl.GLES20; 31 import android.util.Size; 32 import android.view.Surface; 33 import android.view.SurfaceView; 34 import androidx.annotation.Nullable; 35 import com.google.android.exoplayer2.C; 36 import com.google.android.exoplayer2.util.GlUtil; 37 import com.google.android.exoplayer2.util.Util; 38 import com.google.common.collect.ImmutableList; 39 import java.io.IOException; 40 import java.util.List; 41 import java.util.concurrent.ConcurrentLinkedQueue; 42 import java.util.concurrent.ExecutionException; 43 import java.util.concurrent.ExecutorService; 44 import java.util.concurrent.Future; 45 import java.util.concurrent.RejectedExecutionException; 46 import java.util.concurrent.atomic.AtomicInteger; 47 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 48 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 49 50 /** 51 * {@code FrameProcessorChain} applies changes to individual video frames. 52 * 53 * <p>Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously 54 * and is processed on a background thread as it becomes available. All input frames should be 55 * {@linkplain #registerInputFrame() registered} before they are rendered to the input surface. 56 * {@link #getPendingFrameCount()} can be used to check whether there are frames that have not been 57 * fully processed yet. Output is written to its {@linkplain #setOutputSurface(Surface, int, int, 58 * SurfaceView) output surface}. 59 */ 60 /* package */ final class FrameProcessorChain { 61 62 static { 63 GlUtil.glAssertionsEnabled = true; 64 } 65 66 /** 67 * Creates a new instance. 68 * 69 * @param context A {@link Context}. 70 * @param pixelWidthHeightRatio The ratio of width over height, for each pixel. 71 * @param inputWidth The input frame width, in pixels. 72 * @param inputHeight The input frame height, in pixels. 73 * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame. 74 * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. 75 * @return A new instance. 76 * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1, reading shader 77 * files fails, or an OpenGL error occurs while creating and configuring the OpenGL 78 * components. 79 */ create( Context context, float pixelWidthHeightRatio, int inputWidth, int inputHeight, List<GlFrameProcessor> frameProcessors, boolean enableExperimentalHdrEditing)80 public static FrameProcessorChain create( 81 Context context, 82 float pixelWidthHeightRatio, 83 int inputWidth, 84 int inputHeight, 85 List<GlFrameProcessor> frameProcessors, 86 boolean enableExperimentalHdrEditing) 87 throws TransformationException { 88 if (pixelWidthHeightRatio != 1.0f) { 89 // TODO(b/211782176): Consider implementing support for non-square pixels. 90 throw TransformationException.createForFrameProcessorChain( 91 new UnsupportedOperationException( 92 "Transformer's FrameProcessorChain currently does not support frame edits on" 93 + " non-square pixels. The pixelWidthHeightRatio is: " 94 + pixelWidthHeightRatio), 95 TransformationException.ERROR_CODE_GL_INIT_FAILED); 96 } 97 98 ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); 99 ExternalCopyFrameProcessor externalCopyFrameProcessor = 100 new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); 101 102 try { 103 return singleThreadExecutorService 104 .submit( 105 () -> 106 createOpenGlObjectsAndFrameProcessorChain( 107 inputWidth, 108 inputHeight, 109 frameProcessors, 110 enableExperimentalHdrEditing, 111 singleThreadExecutorService, 112 externalCopyFrameProcessor)) 113 .get(); 114 } catch (ExecutionException e) { 115 throw TransformationException.createForFrameProcessorChain( 116 e, TransformationException.ERROR_CODE_GL_INIT_FAILED); 117 } catch (InterruptedException e) { 118 Thread.currentThread().interrupt(); 119 throw TransformationException.createForFrameProcessorChain( 120 e, TransformationException.ERROR_CODE_GL_INIT_FAILED); 121 } 122 } 123 124 /** 125 * Creates the OpenGL textures, framebuffers, initializes the {@link GlFrameProcessor 126 * GlFrameProcessors} and returns a new {@code FrameProcessorChain}. 127 * 128 * <p>This method must by executed using the {@code singleThreadExecutorService}. 129 */ createOpenGlObjectsAndFrameProcessorChain( int inputWidth, int inputHeight, List<GlFrameProcessor> frameProcessors, boolean enableExperimentalHdrEditing, ExecutorService singleThreadExecutorService, ExternalCopyFrameProcessor externalCopyFrameProcessor)130 private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain( 131 int inputWidth, 132 int inputHeight, 133 List<GlFrameProcessor> frameProcessors, 134 boolean enableExperimentalHdrEditing, 135 ExecutorService singleThreadExecutorService, 136 ExternalCopyFrameProcessor externalCopyFrameProcessor) 137 throws IOException { 138 checkState(Thread.currentThread().getName().equals(THREAD_NAME)); 139 140 EGLDisplay eglDisplay = GlUtil.createEglDisplay(); 141 EGLContext eglContext = 142 enableExperimentalHdrEditing 143 ? GlUtil.createEglContextEs3Rgba1010102(eglDisplay) 144 : GlUtil.createEglContext(eglDisplay); 145 146 if (GlUtil.isSurfacelessContextExtensionSupported()) { 147 GlUtil.focusEglSurface( 148 eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); 149 } else if (enableExperimentalHdrEditing) { 150 // TODO(b/209404935): Don't assume BT.2020 PQ input/output. 151 GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay); 152 } else { 153 GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); 154 } 155 156 int inputExternalTexId = GlUtil.createExternalTexture(); 157 externalCopyFrameProcessor.initialize(inputExternalTexId, inputWidth, inputHeight); 158 159 int[] framebuffers = new int[frameProcessors.size()]; 160 Size inputSize = externalCopyFrameProcessor.getOutputSize(); 161 for (int i = 0; i < frameProcessors.size(); i++) { 162 int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight()); 163 framebuffers[i] = GlUtil.createFboForTexture(inputTexId); 164 frameProcessors.get(i).initialize(inputTexId, inputSize.getWidth(), inputSize.getHeight()); 165 inputSize = frameProcessors.get(i).getOutputSize(); 166 } 167 return new FrameProcessorChain( 168 eglDisplay, 169 eglContext, 170 singleThreadExecutorService, 171 inputExternalTexId, 172 framebuffers, 173 new ImmutableList.Builder<GlFrameProcessor>() 174 .add(externalCopyFrameProcessor) 175 .addAll(frameProcessors) 176 .build(), 177 enableExperimentalHdrEditing); 178 } 179 180 private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; 181 182 private final boolean enableExperimentalHdrEditing; 183 private final EGLDisplay eglDisplay; 184 private final EGLContext eglContext; 185 /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */ 186 private final ExecutorService singleThreadExecutorService; 187 /** Futures corresponding to the executor service's pending tasks. */ 188 private final ConcurrentLinkedQueue<Future<?>> futures; 189 /** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */ 190 private final AtomicInteger pendingFrameCount; 191 192 /** Wraps the {@link #inputSurfaceTexture}. */ 193 private final Surface inputSurface; 194 /** Associated with an OpenGL external texture. */ 195 private final SurfaceTexture inputSurfaceTexture; 196 /** Transformation matrix associated with the {@link #inputSurfaceTexture}. */ 197 private final float[] textureTransformMatrix; 198 199 /** 200 * Contains an {@link ExternalCopyFrameProcessor} at the 0th index and optionally other {@link 201 * GlFrameProcessor GlFrameProcessors} at indices >= 1. 202 */ 203 private final ImmutableList<GlFrameProcessor> frameProcessors; 204 /** 205 * Identifiers of a framebuffer object associated with the intermediate textures that receive 206 * output from the previous {@link GlFrameProcessor}, and provide input for the following {@link 207 * GlFrameProcessor}. 208 */ 209 private final int[] framebuffers; 210 211 private Size outputSize; 212 /** 213 * Wraps the output {@link Surface} that is populated with the output of the final {@link 214 * GlFrameProcessor} for each frame. 215 */ 216 private @MonotonicNonNull EGLSurface eglSurface; 217 218 private int debugPreviewWidth; 219 private int debugPreviewHeight; 220 /** 221 * Wraps a debug {@link SurfaceView} that is populated with the output of the final {@link 222 * GlFrameProcessor} for each frame. 223 */ 224 private @MonotonicNonNull EGLSurface debugPreviewEglSurface; 225 226 private boolean inputStreamEnded; 227 /** Prevents further frame processing tasks from being scheduled after {@link #release()}. */ 228 private volatile boolean releaseRequested; 229 FrameProcessorChain( EGLDisplay eglDisplay, EGLContext eglContext, ExecutorService singleThreadExecutorService, int inputExternalTexId, int[] framebuffers, ImmutableList<GlFrameProcessor> frameProcessors, boolean enableExperimentalHdrEditing)230 private FrameProcessorChain( 231 EGLDisplay eglDisplay, 232 EGLContext eglContext, 233 ExecutorService singleThreadExecutorService, 234 int inputExternalTexId, 235 int[] framebuffers, 236 ImmutableList<GlFrameProcessor> frameProcessors, 237 boolean enableExperimentalHdrEditing) { 238 checkState(!frameProcessors.isEmpty()); 239 240 this.eglDisplay = eglDisplay; 241 this.eglContext = eglContext; 242 this.singleThreadExecutorService = singleThreadExecutorService; 243 this.framebuffers = framebuffers; 244 this.frameProcessors = frameProcessors; 245 this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; 246 247 futures = new ConcurrentLinkedQueue<>(); 248 pendingFrameCount = new AtomicInteger(); 249 inputSurfaceTexture = new SurfaceTexture(inputExternalTexId); 250 inputSurface = new Surface(inputSurfaceTexture); 251 textureTransformMatrix = new float[16]; 252 outputSize = getLast(frameProcessors).getOutputSize(); 253 debugPreviewWidth = C.LENGTH_UNSET; 254 debugPreviewHeight = C.LENGTH_UNSET; 255 } 256 257 /** Returns the output {@link Size}. */ getOutputSize()258 public Size getOutputSize() { 259 return outputSize; 260 } 261 262 /** 263 * Sets the output {@link Surface}. 264 * 265 * <p>This method may override the output size of the final {@link GlFrameProcessor}. 266 * 267 * @param outputSurface The output {@link Surface}. 268 * @param outputWidth The output width, in pixels. 269 * @param outputHeight The output height, in pixels. 270 * @param debugSurfaceView Optional debug {@link SurfaceView} to show output. 271 */ setOutputSurface( Surface outputSurface, int outputWidth, int outputHeight, @Nullable SurfaceView debugSurfaceView)272 public void setOutputSurface( 273 Surface outputSurface, 274 int outputWidth, 275 int outputHeight, 276 @Nullable SurfaceView debugSurfaceView) { 277 // TODO(b/218488308): Don't override output size for encoder fallback. Instead allow the final 278 // GlFrameProcessor to be re-configured or append another GlFrameProcessor. 279 outputSize = new Size(outputWidth, outputHeight); 280 281 if (debugSurfaceView != null) { 282 debugPreviewWidth = debugSurfaceView.getWidth(); 283 debugPreviewHeight = debugSurfaceView.getHeight(); 284 } 285 286 futures.add( 287 singleThreadExecutorService.submit( 288 () -> createOpenGlSurfaces(outputSurface, debugSurfaceView))); 289 290 inputSurfaceTexture.setOnFrameAvailableListener( 291 surfaceTexture -> { 292 if (releaseRequested) { 293 // Frames can still become available after a transformation is cancelled but they can be 294 // ignored. 295 return; 296 } 297 try { 298 futures.add(singleThreadExecutorService.submit(this::processFrame)); 299 } catch (RejectedExecutionException e) { 300 if (!releaseRequested) { 301 throw e; 302 } 303 } 304 }); 305 } 306 307 /** Returns the input {@link Surface}. */ getInputSurface()308 public Surface getInputSurface() { 309 return inputSurface; 310 } 311 312 /** 313 * Informs the {@code FrameProcessorChain} that a frame will be queued to its input surface. 314 * 315 * <p>Should be called before rendering a frame to the frame processor chain's input surface. 316 * 317 * @throws IllegalStateException If called after {@link #signalEndOfInputStream()}. 318 */ registerInputFrame()319 public void registerInputFrame() { 320 checkState(!inputStreamEnded); 321 pendingFrameCount.incrementAndGet(); 322 } 323 324 /** 325 * Checks whether any exceptions occurred during asynchronous frame processing and rethrows the 326 * first exception encountered. 327 */ getAndRethrowBackgroundExceptions()328 public void getAndRethrowBackgroundExceptions() throws TransformationException { 329 @Nullable Future<?> oldestGlProcessingFuture = futures.peek(); 330 while (oldestGlProcessingFuture != null && oldestGlProcessingFuture.isDone()) { 331 futures.poll(); 332 try { 333 oldestGlProcessingFuture.get(); 334 } catch (ExecutionException e) { 335 throw TransformationException.createForFrameProcessorChain( 336 e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED); 337 } catch (InterruptedException e) { 338 Thread.currentThread().interrupt(); 339 throw TransformationException.createForFrameProcessorChain( 340 e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED); 341 } 342 oldestGlProcessingFuture = futures.peek(); 343 } 344 } 345 346 /** 347 * Returns the number of input frames that have been {@linkplain #registerInputFrame() registered} 348 * but not completely processed yet. 349 */ getPendingFrameCount()350 public int getPendingFrameCount() { 351 return pendingFrameCount.get(); 352 } 353 354 /** Returns whether all frames have been processed. */ isEnded()355 public boolean isEnded() { 356 return inputStreamEnded && getPendingFrameCount() == 0; 357 } 358 359 /** Informs the {@code FrameProcessorChain} that no further input frames should be accepted. */ signalEndOfInputStream()360 public void signalEndOfInputStream() { 361 inputStreamEnded = true; 362 } 363 364 /** 365 * Releases all resources. 366 * 367 * <p>If the frame processor chain is released before it has {@linkplain #isEnded() ended}, it 368 * will attempt to cancel processing any input frames that have already become available. Input 369 * frames that become available after release are ignored. 370 */ release()371 public void release() { 372 releaseRequested = true; 373 while (!futures.isEmpty()) { 374 checkNotNull(futures.poll()).cancel(/* mayInterruptIfRunning= */ true); 375 } 376 futures.add( 377 singleThreadExecutorService.submit( 378 () -> { 379 for (int i = 0; i < frameProcessors.size(); i++) { 380 frameProcessors.get(i).release(); 381 } 382 GlUtil.destroyEglContext(eglDisplay, eglContext); 383 })); 384 if (inputSurfaceTexture != null) { 385 inputSurfaceTexture.release(); 386 } 387 if (inputSurface != null) { 388 inputSurface.release(); 389 } 390 singleThreadExecutorService.shutdown(); 391 } 392 393 /** 394 * Creates the OpenGL surfaces. 395 * 396 * <p>This method should only be called on the {@linkplain #THREAD_NAME background thread}. 397 */ createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView)398 private void createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView) { 399 checkState(Thread.currentThread().getName().equals(THREAD_NAME)); 400 checkStateNotNull(eglDisplay); 401 402 if (enableExperimentalHdrEditing) { 403 // TODO(b/209404935): Don't assume BT.2020 PQ input/output. 404 eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurface); 405 if (debugSurfaceView != null) { 406 debugPreviewEglSurface = 407 GlUtil.getEglSurfaceBt2020Pq(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); 408 } 409 } else { 410 eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); 411 if (debugSurfaceView != null) { 412 debugPreviewEglSurface = 413 GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); 414 } 415 } 416 } 417 418 /** 419 * Processes an input frame. 420 * 421 * <p>This method should only be called on the {@linkplain #THREAD_NAME background thread}. 422 */ 423 @RequiresNonNull("inputSurfaceTexture") processFrame()424 private void processFrame() { 425 checkState(Thread.currentThread().getName().equals(THREAD_NAME)); 426 checkStateNotNull(eglSurface, "No output surface set."); 427 428 inputSurfaceTexture.updateTexImage(); 429 long presentationTimeNs = inputSurfaceTexture.getTimestamp(); 430 long presentationTimeUs = presentationTimeNs / 1000; 431 inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); 432 ((ExternalCopyFrameProcessor) frameProcessors.get(0)) 433 .setTextureTransformMatrix(textureTransformMatrix); 434 435 for (int i = 0; i < frameProcessors.size() - 1; i++) { 436 Size intermediateSize = frameProcessors.get(i).getOutputSize(); 437 GlUtil.focusFramebuffer( 438 eglDisplay, 439 eglContext, 440 eglSurface, 441 framebuffers[i], 442 intermediateSize.getWidth(), 443 intermediateSize.getHeight()); 444 clearOutputFrame(); 445 frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs); 446 } 447 GlUtil.focusEglSurface( 448 eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); 449 clearOutputFrame(); 450 getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); 451 452 EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs); 453 EGL14.eglSwapBuffers(eglDisplay, eglSurface); 454 455 if (debugPreviewEglSurface != null) { 456 GlUtil.focusEglSurface( 457 eglDisplay, eglContext, debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight); 458 clearOutputFrame(); 459 // The four-vertex triangle strip forms a quad. 460 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); 461 EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface); 462 } 463 464 checkState(pendingFrameCount.getAndDecrement() > 0); 465 } 466 clearOutputFrame()467 private static void clearOutputFrame() { 468 GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); 469 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 470 GlUtil.checkGlError(); 471 } 472 } 473