• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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