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.media.cts;
18 
19 import static org.junit.Assert.fail;
20 
21 import android.media.AudioFormat;
22 import android.media.MediaCodec;
23 import android.media.MediaCodec.BufferInfo;
24 import android.media.MediaExtractor;
25 import android.media.MediaFormat;
26 import android.util.Log;
27 
28 import java.io.BufferedInputStream;
29 import java.io.Closeable;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.nio.ByteBuffer;
33 import java.nio.FloatBuffer;
34 import java.nio.ShortBuffer;
35 
36 public class StreamUtils {
37     private static final String TAG = "CtsMediaStreamUtils";
38 
39     public static abstract class ByteBufferStream {
read()40         public abstract ByteBuffer read() throws IOException;
41     }
42 
43     public static class MediaCodecStream extends ByteBufferStream implements Closeable {
44         private ByteBufferStream mBufferInputStream;
45         private InputStream mInputStream;
46         private MediaCodec mCodec;
47         public boolean mIsFloat;
48         BufferInfo mInfo = new BufferInfo();
49         boolean mSawOutputEOS;
50         boolean mSawInputEOS;
51         boolean mEncode;
52         boolean mSentConfig;
53 
54         // result stream
55         private byte[] mBuf = null;
56         private byte[] mCache = null;
57         private int mBufIn = 0;
58         private int mBufOut = 0;
59         private int mBufCounter = 0;
60 
61         private MediaExtractor mExtractor; // Read from Extractor instead of InputStream
62         // helper for bytewise read()
63         private byte[] mOneByte = new byte[1];
64 
MediaCodecStream( MediaFormat format, boolean encode)65         public MediaCodecStream(
66                 MediaFormat format,
67                 boolean encode) throws Exception {
68             String mime = format.getString(MediaFormat.KEY_MIME);
69             mEncode = encode;
70             if (mEncode) {
71                 mCodec = MediaCodec.createEncoderByType(mime);
72             } else {
73                 mCodec = MediaCodec.createDecoderByType(mime);
74             }
75             mCodec.configure(format,null, null, encode ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0);
76 
77             // check if float
78             final MediaFormat actualFormat =
79                     encode ? mCodec.getInputFormat() : mCodec.getOutputFormat();
80 
81             mIsFloat = actualFormat.getInteger(
82                     MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
83                             == AudioFormat.ENCODING_PCM_FLOAT;
84 
85             mCodec.start();
86         }
87 
MediaCodecStream( InputStream input, MediaFormat format, boolean encode)88         public MediaCodecStream(
89                 InputStream input,
90                 MediaFormat format,
91                 boolean encode) throws Exception {
92             this(format, encode);
93             mInputStream = input;
94         }
95 
MediaCodecStream( ByteBufferStream input, MediaFormat format, boolean encode)96         public MediaCodecStream(
97                 ByteBufferStream input,
98                 MediaFormat format,
99                 boolean encode) throws Exception {
100             this(format, encode);
101             mBufferInputStream = input;
102         }
103 
MediaCodecStream(MediaExtractor mediaExtractor, MediaFormat format)104         public MediaCodecStream(MediaExtractor mediaExtractor,
105                 MediaFormat format) throws Exception {
106             this(format, false /* encode */);
107             mExtractor = mediaExtractor;
108         }
109 
110         @Override
read()111         public ByteBuffer read() throws IOException {
112 
113             if (mSawOutputEOS) {
114                 return null;
115             }
116 
117             // first push as much data into the codec as possible
118             while (!mSawInputEOS) {
119                 Log.i(TAG, "sending data to " + mCodec.getName());
120                 int index = mCodec.dequeueInputBuffer(5000);
121                 if (index < 0) {
122                     // no input buffer currently available
123                     break;
124                 } else {
125                     ByteBuffer buf = mCodec.getInputBuffer(index);
126                     buf.clear();
127                     int inBufLen = buf.limit();
128                     int numRead = 0;
129                     long timestampUs = 0; // non-zero for MediaExtractor mode
130                     if (mExtractor != null) {
131                         numRead = mExtractor.readSampleData(buf, 0 /* offset */);
132                         timestampUs = mExtractor.getSampleTime();
133                         Log.v(TAG, "MediaCodecStream.read using Extractor, numRead "
134                                 + numRead +" timestamp " + timestampUs);
135                         mExtractor.advance();
136                         if(numRead < 0) {
137                            mSawInputEOS = true;
138                            timestampUs = 0;
139                            numRead =0;
140                         }
141                     } else if (mBufferInputStream != null) {
142                         ByteBuffer in = null;
143                         do {
144                             in = mBufferInputStream.read();
145                         } while (in != null && in.limit() - in.position() == 0);
146                         if (in == null) {
147                             mSawInputEOS = true;
148                         } else {
149                             final int n = in.limit() - in.position();
150                             numRead += n;
151                             buf.put(in);
152                         }
153                     } else if (mInputStream != null) {
154                         if (mBuf == null) {
155                             mBuf = new byte[inBufLen];
156                         }
157                         for (numRead = 0; numRead < inBufLen; ) {
158                             int n = mInputStream.read(mBuf, numRead, inBufLen - numRead);
159                             if (n == -1) {
160                                 mSawInputEOS = true;
161                                 break;
162                             }
163                             numRead += n;
164                         }
165                         buf.put(mBuf, 0, numRead);
166                     } else {
167                         fail("no input");
168                     }
169 
170                     int flags = mSawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0;
171                     if (!mEncode && !mSentConfig && mExtractor == null) {
172                         flags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
173                         mSentConfig = true;
174                     }
175                     Log.i(TAG, "queuing input buffer " + index +
176                             ", size " + numRead + ", flags: " + flags +
177                             " on " + mCodec.getName());
178                     mCodec.queueInputBuffer(index,
179                             0 /* offset */,
180                             numRead,
181                             timestampUs /* presentationTimeUs */,
182                             flags);
183                     Log.i(TAG, "queued input buffer " + index + ", size " + numRead);
184                 }
185             }
186 
187             // now read data from the codec
188             Log.i(TAG, "reading from " + mCodec.getName());
189             int index = mCodec.dequeueOutputBuffer(mInfo, 5000);
190             if (index >= 0) {
191                 Log.i(TAG, "got " + mInfo.size + " bytes from " + mCodec.getName());
192                 ByteBuffer out = mCodec.getOutputBuffer(index);
193                 ByteBuffer ret = ByteBuffer.allocate(mInfo.size);
194                 ret.put(out);
195                 mCodec.releaseOutputBuffer(index,  false /* render */);
196                 if ((mInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
197                     Log.i(TAG, "saw output EOS on " + mCodec.getName());
198                     mSawOutputEOS = true;
199                 }
200                 ret.flip(); // prepare buffer for reading from it
201                 // XXX chck that first encoded buffer has CSD flags set
202                 if (mEncode && mBufCounter++ == 0 && (mInfo.flags &
203                     MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
204                     fail("first encoded buffer missing CSD flag");
205                 }
206                 return ret;
207             } else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
208                 Log.i(TAG, mCodec.getName() + " new format: " + mCodec.getOutputFormat());
209             }
210             return ByteBuffer.allocate(0);
211         }
212 
213         @Override
close()214         public void close() throws IOException {
215             try {
216                 if (mInputStream != null) {
217                     mInputStream.close();
218                 }
219             } finally {
220                 mInputStream = null;
221                 try {
222                     if (mCodec != null) {
223                         mCodec.release();
224                     }
225                 } finally {
226                     mCodec = null;
227                 }
228             }
229         }
230 
231         @Override
finalize()232         protected void finalize() throws Throwable {
233             if (mCodec != null) {
234                 Log.w(TAG, "MediaCodecInputStream wasn't closed");
235                 mCodec.release();
236             }
237         }
238     };
239 
240     public static class ByteBufferInputStream extends InputStream {
241         ByteBufferStream mInput;
242         ByteBuffer mBuffer;
243 
ByteBufferInputStream(ByteBufferStream in)244         public ByteBufferInputStream(ByteBufferStream in) {
245             mInput = in;
246             mBuffer = ByteBuffer.allocate(0);
247         }
248 
249         @Override
read()250         public int read() throws IOException {
251             while (mBuffer != null && !mBuffer.hasRemaining()) {
252                 Log.i(TAG, "reading buffer");
253                 mBuffer = mInput.read();
254             }
255 
256             if (mBuffer == null) {
257                 return -1;
258             }
259 
260             return (0xff & mBuffer.get());
261         }
262     };
263 
264     public static class PcmAudioBufferStream extends ByteBufferStream {
265 
266         public int mCount;         // either 0 or 1 if the buffer has been delivered
267         public ByteBuffer mBuffer; // the audio buffer (furnished duplicated, read only).
268 
PcmAudioBufferStream( int samples, int sampleRate, double frequency, double sweep, boolean useFloat)269         public PcmAudioBufferStream(
270             int samples, int sampleRate, double frequency, double sweep, boolean useFloat) {
271             final int sampleSize = useFloat ? 4 : 2;
272             final int sizeInBytes = samples * sampleSize;
273             mBuffer = ByteBuffer.allocate(sizeInBytes);
274             mBuffer.order(java.nio.ByteOrder.nativeOrder());
275             if (useFloat) {
276                 FloatBuffer fb = mBuffer.asFloatBuffer();
277                 float[] fa = AudioHelper.createSoundDataInFloatArray(
278                     samples, sampleRate, frequency, sweep);
279                 for (int i = 0; i < fa.length; ++i) {
280                     // quantize to a Q.23 integer so that identity is preserved
281                     fa[i] = (float)((int)(fa[i] * ((1 << 23) - 1))) / (1 << 23);
282                 }
283                 fb.put(fa);
284             } else {
285                 ShortBuffer sb = mBuffer.asShortBuffer();
286                 sb.put(AudioHelper.createSoundDataInShortArray(
287                     samples, sampleRate, frequency, sweep));
288             }
289             mBuffer.limit(sizeInBytes);
290         }
291 
292         // duplicating constructor
PcmAudioBufferStream(PcmAudioBufferStream other)293         public PcmAudioBufferStream(PcmAudioBufferStream other) {
294             mCount = 0;
295             mBuffer = other.mBuffer; // ok to copy, furnished read-only
296         }
297 
sizeInBytes()298         public int sizeInBytes() {
299             return mBuffer.capacity();
300         }
301 
302         @Override
read()303         public ByteBuffer read() throws IOException {
304             if (mCount < 1 /* only one buffer */) {
305                 ++mCount;
306                 return mBuffer.asReadOnlyBuffer();
307             }
308             return null;
309         }
310     }
311 
compareStreams(InputStream test, InputStream reference)312     public static int compareStreams(InputStream test, InputStream reference) {
313         Log.i(TAG, "compareStreams");
314         BufferedInputStream buffered_test = new BufferedInputStream(test);
315         BufferedInputStream buffered_reference = new BufferedInputStream(reference);
316         int numread = 0;
317         try {
318             while (true) {
319                 int b1 = buffered_test.read();
320                 int b2 = buffered_reference.read();
321                 if (b1 != b2) {
322                     Log.e(TAG, "streams differ at " + numread + ": " + b1 + "/" + b2);
323                     return -1;
324                 }
325                 if (b1 == -1) {
326                     Log.e(TAG, "streams ended at " + numread);
327                     break;
328                 }
329                 numread++;
330             }
331         } catch (Exception e) {
332             Log.e(TAG, "read error", e);
333             return -1;
334         }
335         Log.i(TAG, "compareStreams read " + numread);
336         return numread;
337     }
338 }
339