• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.M;
4 import static android.os.Build.VERSION_CODES.N;
5 import static android.os.Build.VERSION_CODES.O;
6 import static java.lang.Math.min;
7 import static org.robolectric.shadows.util.DataSource.toDataSource;
8 
9 import android.content.Context;
10 import android.content.res.AssetFileDescriptor;
11 import android.media.MediaDataSource;
12 import android.media.MediaExtractor;
13 import android.media.MediaFormat;
14 import android.net.Uri;
15 import android.os.PersistableBundle;
16 import java.io.FileDescriptor;
17 import java.nio.ByteBuffer;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import org.robolectric.annotation.Implementation;
24 import org.robolectric.annotation.Implements;
25 import org.robolectric.annotation.Resetter;
26 import org.robolectric.shadows.util.DataSource;
27 
28 /**
29  * A shadow for the MediaExtractor class.
30  *
31  * <p>Returns data previously injected by {@link #addTrack(DataSource, MediaFormat, byte[])}.
32  *
33  * <p>Note several limitations, due to not using actual media codecs for decoding:
34  *
35  * <ul>
36  *   <li>Only one track may be selected at a time; multi-track selection is not supported.
37  *   <li>{@link #advance()} will advance by the size of the last read (i.e. the return value of the
38  *       last call to {@link #readSampleData(ByteBuffer, int)}).
39  *   <li>{@link MediaExtractor#getSampleTime()} and {@link MediaExtractor#getSampleSize()} are
40  *       unimplemented.
41  *   <li>{@link MediaExtractor#seekTo()} is unimplemented.
42  * </ul>
43  */
44 @Implements(MediaExtractor.class)
45 public class ShadowMediaExtractor {
46 
47   private static class TrackInfo {
48     MediaFormat format;
49     byte[] sampleData;
50   }
51 
52   private static final Map<DataSource, List<TrackInfo>> tracksMap = new HashMap<>();
53   private static final Map<DataSource, PersistableBundle> metricsMap = new HashMap<>();
54 
55   private List<TrackInfo> tracks;
56   private PersistableBundle metrics;
57   private int[] trackSampleReadPositions;
58   private int[] trackLastReadSize;
59   private int selectedTrackIndex = -1;
60 
61   /**
62    * Adds a track of data to an associated {@link org.robolectric.shadows.util.DataSource}.
63    *
64    * @param format the format which will be returned by {@link MediaExtractor#getTrackFormat(int)}
65    * @param sampleData the data which will be iterated upon and returned by {@link
66    *     MediaExtractor#readSampleData(ByteBuffer, int)}.
67    */
addTrack(DataSource dataSource, MediaFormat format, byte[] sampleData)68   public static void addTrack(DataSource dataSource, MediaFormat format, byte[] sampleData) {
69     TrackInfo trackInfo = new TrackInfo();
70     trackInfo.format = format;
71     trackInfo.sampleData = sampleData;
72     tracksMap.putIfAbsent(dataSource, new ArrayList<TrackInfo>());
73     List<TrackInfo> tracks = tracksMap.get(dataSource);
74     tracks.add(trackInfo);
75   }
76 
77   /**
78    * Sets metrics for an associated {@link org.robolectric.shadows.util.DataSource}.
79    *
80    * @param metrics the data which will be returned by {@link MediaExtractor#getMetrics()}.
81    */
setMetrics(DataSource dataSource, PersistableBundle metrics)82   public static void setMetrics(DataSource dataSource, PersistableBundle metrics) {
83     metricsMap.put(dataSource, metrics);
84   }
85 
setDataSource(DataSource dataSource)86   private void setDataSource(DataSource dataSource) {
87     if (tracksMap.containsKey(dataSource)) {
88       this.tracks = tracksMap.get(dataSource);
89     } else {
90       this.tracks = new ArrayList<>();
91     }
92 
93     this.trackSampleReadPositions = new int[tracks.size()];
94     Arrays.fill(trackSampleReadPositions, 0);
95     this.trackLastReadSize = new int[tracks.size()];
96     Arrays.fill(trackLastReadSize, 0);
97 
98     this.metrics = metricsMap.get(dataSource);
99   }
100 
101   @Implementation(minSdk = N)
setDataSource(AssetFileDescriptor assetFileDescriptor)102   protected void setDataSource(AssetFileDescriptor assetFileDescriptor) {
103     setDataSource(toDataSource(assetFileDescriptor));
104   }
105 
106   @Implementation
setDataSource(Context context, Uri uri, Map<String, String> headers)107   protected void setDataSource(Context context, Uri uri, Map<String, String> headers) {
108     setDataSource(toDataSource(context, uri, headers));
109   }
110 
111   @Implementation
setDataSource(FileDescriptor fileDescriptor)112   protected void setDataSource(FileDescriptor fileDescriptor) {
113     setDataSource(toDataSource(fileDescriptor));
114   }
115 
116   @Implementation(minSdk = M)
setDataSource(MediaDataSource mediaDataSource)117   protected void setDataSource(MediaDataSource mediaDataSource) {
118     setDataSource(toDataSource(mediaDataSource));
119   }
120 
121   @Implementation
setDataSource(FileDescriptor fileDescriptor, long offset, long length)122   protected void setDataSource(FileDescriptor fileDescriptor, long offset, long length) {
123     setDataSource(toDataSource(fileDescriptor, offset, length));
124   }
125 
126   @Implementation
setDataSource(String path)127   protected void setDataSource(String path) {
128     setDataSource(toDataSource(path));
129   }
130 
131   @Implementation
setDataSource(String path, Map<String, String> headers)132   protected void setDataSource(String path, Map<String, String> headers) {
133     setDataSource(toDataSource(path));
134   }
135 
136   @Implementation
advance()137   protected boolean advance() {
138     if (selectedTrackIndex == -1) {
139       throw new IllegalStateException("Called advance() with no selected track");
140     }
141 
142     int readPosition = trackSampleReadPositions[selectedTrackIndex];
143     int trackDataLength = tracks.get(selectedTrackIndex).sampleData.length;
144     if (readPosition >= trackDataLength) {
145       return false;
146     }
147 
148     trackSampleReadPositions[selectedTrackIndex] += trackLastReadSize[selectedTrackIndex];
149     return true;
150   }
151 
152   @Implementation
getSampleTrackIndex()153   protected int getSampleTrackIndex() {
154     return selectedTrackIndex;
155   }
156 
157   @Implementation
getTrackCount()158   protected int getTrackCount() {
159     return tracks.size();
160   }
161 
162   @Implementation
getTrackFormat(int index)163   protected MediaFormat getTrackFormat(int index) {
164     if (index >= tracks.size()) {
165       throw new ArrayIndexOutOfBoundsException(
166           "Called getTrackFormat() with index:"
167               + index
168               + ", beyond number of tracks:"
169               + tracks.size());
170     }
171 
172     return tracks.get(index).format;
173   }
174 
175   @Implementation
readSampleData(ByteBuffer byteBuf, int offset)176   protected int readSampleData(ByteBuffer byteBuf, int offset) {
177     if (selectedTrackIndex == -1) {
178       return 0;
179     }
180     int currentReadPosition = trackSampleReadPositions[selectedTrackIndex];
181     TrackInfo trackInfo = tracks.get(selectedTrackIndex);
182     int trackDataLength = trackInfo.sampleData.length;
183     if (currentReadPosition >= trackDataLength) {
184       return -1;
185     }
186 
187     int length = min(byteBuf.capacity(), trackDataLength - currentReadPosition);
188     byteBuf.put(trackInfo.sampleData, currentReadPosition, length);
189     trackLastReadSize[selectedTrackIndex] = length;
190     return length;
191   }
192 
193   @Implementation
selectTrack(int index)194   protected void selectTrack(int index) {
195     if (selectedTrackIndex != -1) {
196       throw new IllegalStateException(
197           "Called selectTrack() when there is already a track selected; call unselectTrack() first."
198               + " ShadowMediaExtractor does not support multiple track selection.");
199     }
200     if (index >= tracks.size()) {
201       throw new ArrayIndexOutOfBoundsException(
202           "Called selectTrack() with index:"
203               + index
204               + ", beyond number of tracks:"
205               + tracks.size());
206     }
207 
208     selectedTrackIndex = index;
209   }
210 
211   @Implementation
unselectTrack(int index)212   protected void unselectTrack(int index) {
213     if (selectedTrackIndex != index) {
214       throw new IllegalStateException(
215           "Called unselectTrack() on a track other than the single selected track."
216               + " ShadowMediaExtractor does not support multiple track selection.");
217     }
218     selectedTrackIndex = -1;
219   }
220 
221   @Implementation(minSdk = O)
getMetrics()222   protected PersistableBundle getMetrics() {
223     return metrics;
224   }
225 
226   @Resetter
reset()227   public static void reset() {
228     tracksMap.clear();
229     metricsMap.clear();
230     DataSource.reset();
231   }
232 }
233