1 /*
2  * Copyright (C) 2023 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.assertNotNull;
20 import static org.junit.Assert.assertNull;
21 import static org.junit.Assert.assertTrue;
22 
23 import android.media.Image;
24 import android.media.ImageReader;
25 import android.os.Handler;
26 import android.os.HandlerThread;
27 import android.util.Log;
28 import android.util.Pair;
29 import android.view.Surface;
30 
31 import java.util.ArrayDeque;
32 import java.util.Locale;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.locks.Condition;
35 import java.util.concurrent.locks.Lock;
36 import java.util.concurrent.locks.ReentrantLock;
37 import java.util.function.Function;
38 
39 /**
40  * Wrapper class to hold surface provided by ImageReader.
41  */
42 public class ImageSurface implements ImageReader.OnImageAvailableListener {
43     private static final String LOG_TAG = ImageSurface.class.getSimpleName();
44 
45     private final ArrayDeque<Pair<Image, Exception>> mQueue = new ArrayDeque<>();
46     private final Lock mLock = new ReentrantLock();
47     private final Condition mCondition = mLock.newCondition();
48     private ImageReader mReader;
49     private Surface mReaderSurface;
50     private HandlerThread mHandlerThread;
51     private Handler mHandler;
52     private int mImageBoundToSurfaceId;
53     private boolean mQueueOverflowed;
54     private Function<ImageAndAttributes, Boolean> mPredicate;
55 
56     public static class ImageAndAttributes {
57         public Image mImage;
58         public int mImageBoundToSurfaceId;
59 
ImageAndAttributes(Image image, int surfaceId)60         public ImageAndAttributes(Image image, int surfaceId) {
61             mImage = image;
62             mImageBoundToSurfaceId = surfaceId;
63         }
64     }
65 
66     @Override
onImageAvailable(ImageReader reader)67     public void onImageAvailable(ImageReader reader) {
68         mLock.lock();
69         try {
70             if (mQueue.size() == reader.getMaxImages()) {
71                 Log.w(LOG_TAG, "image queue is at full capacity, releasing oldest image to"
72                         + " make space for image just received");
73                 releaseImage(mQueue.poll());
74                 mQueueOverflowed = true;
75             }
76             Image image = reader.acquireNextImage();
77             Log.d(LOG_TAG, "received image" + image);
78             mQueue.add(Pair.create(image, null /* Exception */));
79             mCondition.signalAll();
80         } catch (Exception e) {
81             Log.e(LOG_TAG, "Can't handle Exceptions in onImageAvailable " + e);
82             mQueue.add(Pair.create(null /* Image */, e));
83         } finally {
84             mLock.unlock();
85         }
86     }
87 
getImage(long timeout)88     public Image getImage(long timeout) throws InterruptedException {
89         int retry = 3;
90         Image image = null;
91         mLock.lock();
92         try {
93             while (mQueue.size() == 0 && retry > 0) {
94                 if (!mCondition.await(timeout, TimeUnit.MILLISECONDS)) {
95                     retry--;
96                 }
97             }
98             if (mQueue.size() > 0) {
99                 Pair<Image, Exception> imageResult = mQueue.poll();
100                 assertNotNull("bad element in image queue", imageResult);
101                 image = imageResult.first;
102                 Exception e = imageResult.second;
103                 assertNull("onImageAvailable() generated an exception: " + e, e);
104                 assertNotNull("Wait for an image timed out in " + timeout + "ms", image);
105             }
106         } finally {
107             mLock.unlock();
108         }
109         return image;
110     }
111 
createSurface(int width, int height, int format, int maxNumImages, int surfaceId, Function<ImageAndAttributes, Boolean> predicate)112     public void createSurface(int width, int height, int format, int maxNumImages,
113             int surfaceId, Function<ImageAndAttributes, Boolean> predicate) {
114         if (mReader != null) {
115             throw new RuntimeException(
116                     "Current instance of ImageSurface already has a weak reference to some "
117                             + "surface, release older surface or reuse it");
118         }
119         mHandlerThread = new HandlerThread(LOG_TAG);
120         mHandlerThread.start();
121         mHandler = new Handler(mHandlerThread.getLooper());
122         mReader = ImageReader.newInstance(width, height, format, maxNumImages);
123         mReader.setOnImageAvailableListener(this, mHandler);
124         mReaderSurface = mReader.getSurface();
125         mImageBoundToSurfaceId = surfaceId;
126         mQueueOverflowed = false;
127         mPredicate = predicate;
128         Log.v(LOG_TAG, String.format(Locale.getDefault(), "Created ImageReader size (%dx%d),"
129                 + " format %d, maxNumImages %d", width, height, format, maxNumImages));
130     }
131 
getSurface()132     public Surface getSurface() {
133         return mReaderSurface;
134     }
135 
releaseImage(Pair<Image, Exception> imageResult)136     private void releaseImage(Pair<Image, Exception> imageResult) {
137         assertNotNull("bad element in image queue", imageResult);
138         Image image = imageResult.first;
139         Exception e = imageResult.second;
140         assertNull("onImageAvailable() generated an exception: " + e, e);
141         assertNotNull("received null for image", image);
142         if (mPredicate != null) {
143             assertTrue("predicate failed on image instance",
144                     mPredicate.apply(new ImageAndAttributes(image, mImageBoundToSurfaceId)));
145         }
146         image.close();
147     }
148 
hasQueueOverflowed()149     public boolean hasQueueOverflowed() {
150         return mQueueOverflowed;
151     }
152 
release()153     public void release() {
154         mReaderSurface = null;
155         if (mReader != null) {
156             mLock.lock();
157             try {
158                 mQueue.forEach(this::releaseImage);
159                 mQueue.clear();
160                 Image image = mReader.acquireLatestImage();
161                 if (image != null) {
162                     image.close();
163                 }
164             } finally {
165                 mReader.close();
166                 mReader = null;
167                 mLock.unlock();
168             }
169         }
170         if (mHandlerThread != null) {
171             mHandlerThread.quitSafely();
172             mHandlerThread = null;
173         }
174         mHandler = null;
175     }
176 }
177