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