Java tutorial
/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.media.AudioPresentation; import android.media.MediaCodec; import android.media.MediaFormat; import android.media.MediaHTTPService; import android.net.Uri; import android.os.IBinder; import android.os.IHwBinder; import android.os.PersistableBundle; import com.android.internal.util.Preconditions; import java.io.FileDescriptor; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; /** * MediaExtractor facilitates extraction of demuxed, typically encoded, media data * from a data source. * <p>It is generally used like this: * <pre> * MediaExtractor extractor = new MediaExtractor(); * extractor.setDataSource(...); * int numTracks = extractor.getTrackCount(); * for (int i = 0; i < numTracks; ++i) { * MediaFormat format = extractor.getTrackFormat(i); * String mime = format.getString(MediaFormat.KEY_MIME); * if (weAreInterestedInThisTrack) { * extractor.selectTrack(i); * } * } * ByteBuffer inputBuffer = ByteBuffer.allocate(...) * while (extractor.readSampleData(inputBuffer, ...) >= 0) { * int trackIndex = extractor.getSampleTrackIndex(); * long presentationTimeUs = extractor.getSampleTime(); * ... * extractor.advance(); * } * * extractor.release(); * extractor = null; * </pre> * * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission * when used with network-based content. */ final public class MediaExtractor { public MediaExtractor() { native_setup(); } /** * Sets the data source (MediaDataSource) to use. * * @param dataSource the MediaDataSource for the media you want to extract from * * @throws IllegalArgumentException if dataSource is invalid. */ public native final void setDataSource(@NonNull MediaDataSource dataSource) throws IOException; /** * Sets the data source as a content Uri. * * @param context the Context to use when resolving the Uri * @param uri the Content URI of the data you want to extract from. * * <p>When <code>uri</code> refers to a network file the * {@link android.Manifest.permission#INTERNET} permission is required. * * @param headers the headers to be sent together with the request for the data. * This can be {@code null} if no specific headers are to be sent with the * request. */ public final void setDataSource(@NonNull Context context, @NonNull Uri uri, @Nullable Map<String, String> headers) throws IOException { String scheme = uri.getScheme(); if (scheme == null || scheme.equals("file")) { setDataSource(uri.getPath()); return; } AssetFileDescriptor fd = null; try { ContentResolver resolver = context.getContentResolver(); fd = resolver.openAssetFileDescriptor(uri, "r"); if (fd == null) { return; } // Note: using getDeclaredLength so that our behavior is the same // as previous versions when the content provider is returning // a full file. if (fd.getDeclaredLength() < 0) { setDataSource(fd.getFileDescriptor()); } else { setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getDeclaredLength()); } return; } catch (SecurityException ex) { } catch (IOException ex) { } finally { if (fd != null) { fd.close(); } } setDataSource(uri.toString(), headers); } /** * Sets the data source (file-path or http URL) to use. * * @param path the path of the file, or the http URL * * <p>When <code>path</code> refers to a network file the * {@link android.Manifest.permission#INTERNET} permission is required. * * @param headers the headers associated with the http request for the stream you want to play. * This can be {@code null} if no specific headers are to be sent with the * request. */ public final void setDataSource(@NonNull String path, @Nullable Map<String, String> headers) throws IOException { String[] keys = null; String[] values = null; if (headers != null) { keys = new String[headers.size()]; values = new String[headers.size()]; int i = 0; for (Map.Entry<String, String> entry : headers.entrySet()) { keys[i] = entry.getKey(); values[i] = entry.getValue(); ++i; } } nativeSetDataSource(MediaHTTPService.createHttpServiceBinderIfNecessary(path), path, keys, values); } private native final void nativeSetDataSource(@NonNull IBinder httpServiceBinder, @NonNull String path, @Nullable String[] keys, @Nullable String[] values) throws IOException; /** * Sets the data source (file-path or http URL) to use. * * @param path the path of the file, or the http URL of the stream * * <p>When <code>path</code> refers to a local file, the file may actually be opened by a * process other than the calling application. This implies that the pathname * should be an absolute path (as any other process runs with unspecified current working * directory), and that the pathname should reference a world-readable file. * As an alternative, the application could first open the file for reading, * and then use the file descriptor form {@link #setDataSource(FileDescriptor)}. * * <p>When <code>path</code> refers to a network file the * {@link android.Manifest.permission#INTERNET} permission is required. */ public final void setDataSource(@NonNull String path) throws IOException { nativeSetDataSource(MediaHTTPService.createHttpServiceBinderIfNecessary(path), path, null, null); } /** * Sets the data source (AssetFileDescriptor) to use. It is the caller's * responsibility to close the file descriptor. It is safe to do so as soon * as this call returns. * * @param afd the AssetFileDescriptor for the file you want to extract from. */ public final void setDataSource(@NonNull AssetFileDescriptor afd) throws IOException, IllegalArgumentException, IllegalStateException { Preconditions.checkNotNull(afd); // Note: using getDeclaredLength so that our behavior is the same // as previous versions when the content provider is returning // a full file. if (afd.getDeclaredLength() < 0) { setDataSource(afd.getFileDescriptor()); } else { setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getDeclaredLength()); } } /** * Sets the data source (FileDescriptor) to use. It is the caller's responsibility * to close the file descriptor. It is safe to do so as soon as this call returns. * * @param fd the FileDescriptor for the file you want to extract from. */ public final void setDataSource(@NonNull FileDescriptor fd) throws IOException { setDataSource(fd, 0, 0x7ffffffffffffffL); } /** * Sets the data source (FileDescriptor) to use. The FileDescriptor must be * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility * to close the file descriptor. It is safe to do so as soon as this call returns. * * @param fd the FileDescriptor for the file you want to extract from. * @param offset the offset into the file where the data to be extracted starts, in bytes * @param length the length in bytes of the data to be extracted */ public native final void setDataSource(@NonNull FileDescriptor fd, long offset, long length) throws IOException; /** * Sets the MediaCas instance to use. This should be called after a * successful setDataSource() if at least one track reports mime type * of {@link android.media.MediaFormat#MIMETYPE_AUDIO_SCRAMBLED} * or {@link android.media.MediaFormat#MIMETYPE_VIDEO_SCRAMBLED}. * Stream parsing will not proceed until a valid MediaCas object * is provided. * * @param mediaCas the MediaCas object to use. */ public final void setMediaCas(@NonNull MediaCas mediaCas) { mMediaCas = mediaCas; nativeSetMediaCas(mediaCas.getBinder()); } private native final void nativeSetMediaCas(@NonNull IHwBinder casBinder); /** * Describes the conditional access system used to scramble a track. */ public static final class CasInfo { private final int mSystemId; private final MediaCas.Session mSession; CasInfo(int systemId, @Nullable MediaCas.Session session) { mSystemId = systemId; mSession = session; } /** * Retrieves the system id of the conditional access system. * * @return CA system id of the CAS used to scramble the track. */ public int getSystemId() { return mSystemId; } /** * Retrieves the {@link MediaCas.Session} associated with a track. The * session is needed to initialize a descrambler in order to decode the * scrambled track. * <p> * @see MediaDescrambler#setMediaCasSession * <p> * @return a {@link MediaCas.Session} object associated with a track. */ public MediaCas.Session getSession() { return mSession; } } private ArrayList<Byte> toByteArray(@NonNull byte[] data) { ArrayList<Byte> byteArray = new ArrayList<Byte>(data.length); for (int i = 0; i < data.length; i++) { byteArray.add(i, Byte.valueOf(data[i])); } return byteArray; } /** * Retrieves the information about the conditional access system used to scramble * a track. * * @param index of the track. * @return an {@link CasInfo} object describing the conditional access system. */ public CasInfo getCasInfo(int index) { Map<String, Object> formatMap = getTrackFormatNative(index); if (formatMap.containsKey(MediaFormat.KEY_CA_SYSTEM_ID)) { int systemId = ((Integer) formatMap.get(MediaFormat.KEY_CA_SYSTEM_ID)).intValue(); MediaCas.Session session = null; if (mMediaCas != null && formatMap.containsKey(MediaFormat.KEY_CA_SESSION_ID)) { ByteBuffer buf = (ByteBuffer) formatMap.get(MediaFormat.KEY_CA_SESSION_ID); buf.rewind(); final byte[] sessionId = new byte[buf.remaining()]; buf.get(sessionId); session = mMediaCas.createFromSessionId(toByteArray(sessionId)); } return new CasInfo(systemId, session); } return null; } @Override protected void finalize() { native_finalize(); } /** * Make sure you call this when you're done to free up any resources * instead of relying on the garbage collector to do this for you at * some point in the future. */ public native final void release(); /** * Count the number of tracks found in the data source. */ public native final int getTrackCount(); /** * Extract DRM initialization data if it exists * * @return DRM initialization data in the content, or {@code null} * if no recognizable DRM format is found; * @see DrmInitData */ public DrmInitData getDrmInitData() { Map<String, Object> formatMap = getFileFormatNative(); if (formatMap == null) { return null; } if (formatMap.containsKey("pssh")) { Map<UUID, byte[]> psshMap = getPsshInfo(); final Map<UUID, DrmInitData.SchemeInitData> initDataMap = new HashMap<UUID, DrmInitData.SchemeInitData>(); for (Map.Entry<UUID, byte[]> e : psshMap.entrySet()) { UUID uuid = e.getKey(); byte[] data = e.getValue(); initDataMap.put(uuid, new DrmInitData.SchemeInitData("cenc", data)); } return new DrmInitData() { public SchemeInitData get(UUID schemeUuid) { return initDataMap.get(schemeUuid); } }; } else { int numTracks = getTrackCount(); for (int i = 0; i < numTracks; ++i) { Map<String, Object> trackFormatMap = getTrackFormatNative(i); if (!trackFormatMap.containsKey("crypto-key")) { continue; } ByteBuffer buf = (ByteBuffer) trackFormatMap.get("crypto-key"); buf.rewind(); final byte[] data = new byte[buf.remaining()]; buf.get(data); return new DrmInitData() { public SchemeInitData get(UUID schemeUuid) { return new DrmInitData.SchemeInitData("webm", data); } }; } } return null; } /** * Get the list of available audio presentations for the track. * @param trackIndex index of the track. * @return a list of available audio presentations for a given valid audio track index. * The list will be empty if the source does not contain any audio presentations. */ @NonNull public List<AudioPresentation> getAudioPresentations(int trackIndex) { return new ArrayList<AudioPresentation>(); } /** * Get the PSSH info if present. * @return a map of uuid-to-bytes, with the uuid specifying * the crypto scheme, and the bytes being the data specific to that scheme. * This can be {@code null} if the source does not contain PSSH info. */ @Nullable public Map<UUID, byte[]> getPsshInfo() { Map<UUID, byte[]> psshMap = null; Map<String, Object> formatMap = getFileFormatNative(); if (formatMap != null && formatMap.containsKey("pssh")) { ByteBuffer rawpssh = (ByteBuffer) formatMap.get("pssh"); rawpssh.order(ByteOrder.nativeOrder()); rawpssh.rewind(); formatMap.remove("pssh"); // parse the flat pssh bytebuffer into something more manageable psshMap = new HashMap<UUID, byte[]>(); while (rawpssh.remaining() > 0) { rawpssh.order(ByteOrder.BIG_ENDIAN); long msb = rawpssh.getLong(); long lsb = rawpssh.getLong(); UUID uuid = new UUID(msb, lsb); rawpssh.order(ByteOrder.nativeOrder()); int datalen = rawpssh.getInt(); byte[] psshdata = new byte[datalen]; rawpssh.get(psshdata); psshMap.put(uuid, psshdata); } } return psshMap; } @NonNull private native Map<String, Object> getFileFormatNative(); /** * Get the track format at the specified index. * * More detail on the representation can be found at {@link android.media.MediaCodec} * <p> * The following table summarizes support for format keys across android releases: * * <table style="width: 0%"> * <thead> * <tr> * <th rowspan=2>OS Version(s)</th> * <td colspan=3>{@code MediaFormat} keys used for</th> * </tr><tr> * <th>All Tracks</th> * <th>Audio Tracks</th> * <th>Video Tracks</th> * </tr> * </thead> * <tbody> * <tr> * <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN}</td> * <td rowspan=8>{@link MediaFormat#KEY_MIME},<br> * {@link MediaFormat#KEY_DURATION},<br> * {@link MediaFormat#KEY_MAX_INPUT_SIZE}</td> * <td rowspan=5>{@link MediaFormat#KEY_SAMPLE_RATE},<br> * {@link MediaFormat#KEY_CHANNEL_COUNT},<br> * {@link MediaFormat#KEY_CHANNEL_MASK},<br> * gapless playback information<sup>.mp3, .mp4</sup>,<br> * {@link MediaFormat#KEY_IS_ADTS}<sup>AAC if streaming</sup>,<br> * codec-specific data<sup>AAC, Vorbis</sup></td> * <td rowspan=2>{@link MediaFormat#KEY_WIDTH},<br> * {@link MediaFormat#KEY_HEIGHT},<br> * codec-specific data<sup>AVC, MPEG4</sup></td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}</td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}</td> * <td rowspan=3>as above, plus<br> * Pixel aspect ratio information<sup>AVC, *</sup></td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#KITKAT}</td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#KITKAT_WATCH}</td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</td> * <td rowspan=2>as above, plus<br> * {@link MediaFormat#KEY_BIT_RATE}<sup>AAC</sup>,<br> * codec-specific data<sup>Opus</sup></td> * <td rowspan=2>as above, plus<br> * {@link MediaFormat#KEY_ROTATION}<sup>.mp4</sup>,<br> * {@link MediaFormat#KEY_BIT_RATE}<sup>MPEG4</sup>,<br> * codec-specific data<sup>HEVC</sup></td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#M}</td> * <td>as above, plus<br> * gapless playback information<sup>Opus</sup></td> * <td>as above, plus<br> * {@link MediaFormat#KEY_FRAME_RATE} (integer)</td> * </tr><tr> * <td>{@link android.os.Build.VERSION_CODES#N}</td> * <td>as above, plus<br> * {@link MediaFormat#KEY_TRACK_ID},<br> * <!-- {link MediaFormat#KEY_MAX_BIT_RATE}<sup>#, .mp4</sup>,<br> --> * {@link MediaFormat#KEY_BIT_RATE}<sup>#, .mp4</sup></td> * <td>as above, plus<br> * {@link MediaFormat#KEY_PCM_ENCODING},<br> * {@link MediaFormat#KEY_PROFILE}<sup>AAC</sup></td> * <td>as above, plus<br> * {@link MediaFormat#KEY_HDR_STATIC_INFO}<sup>#, .webm</sup>,<br> * {@link MediaFormat#KEY_COLOR_STANDARD}<sup>#</sup>,<br> * {@link MediaFormat#KEY_COLOR_TRANSFER}<sup>#</sup>,<br> * {@link MediaFormat#KEY_COLOR_RANGE}<sup>#</sup>,<br> * {@link MediaFormat#KEY_PROFILE}<sup>MPEG2, H.263, MPEG4, AVC, HEVC, VP9</sup>,<br> * {@link MediaFormat#KEY_LEVEL}<sup>H.263, MPEG4, AVC, HEVC, VP9</sup>,<br> * codec-specific data<sup>VP9</sup></td> * </tr> * <tr> * <td colspan=4> * <p class=note><strong>Notes:</strong><br> * #: container-specified value only.<br> * .mp4, .webm…: for listed containers<br> * MPEG4, AAC…: for listed codecs * </td> * </tr><tr> * <td colspan=4> * <p class=note>Note that that level information contained in the container many times * does not match the level of the actual bitstream. You may want to clear the level using * {@code MediaFormat.setString(KEY_LEVEL, null)} before using the track format to find a * decoder that can play back a particular track. * </td> * </tr><tr> * <td colspan=4> * <p class=note><strong>*Pixel (sample) aspect ratio</strong> is returned in the following * keys. The display width can be calculated for example as: * <p align=center> * display-width = display-height * crop-width / crop-height * sar-width / sar-height * </td> * </tr><tr> * <th>Format Key</th><th>Value Type</th><th colspan=2>Description</th> * </tr><tr> * <td>{@code "sar-width"}</td><td>Integer</td><td colspan=2>Pixel aspect ratio width</td> * </tr><tr> * <td>{@code "sar-height"}</td><td>Integer</td><td colspan=2>Pixel aspect ratio height</td> * </tr> * </tbody> * </table> * */ @NonNull public MediaFormat getTrackFormat(int index) { return new MediaFormat(getTrackFormatNative(index)); } @NonNull private native Map<String, Object> getTrackFormatNative(int index); /** * Subsequent calls to {@link #readSampleData}, {@link #getSampleTrackIndex} and * {@link #getSampleTime} only retrieve information for the subset of tracks * selected. * Selecting the same track multiple times has no effect, the track is * only selected once. */ public native void selectTrack(int index); /** * Subsequent calls to {@link #readSampleData}, {@link #getSampleTrackIndex} and * {@link #getSampleTime} only retrieve information for the subset of tracks * selected. */ public native void unselectTrack(int index); /** * If possible, seek to a sync sample at or before the specified time */ public static final int SEEK_TO_PREVIOUS_SYNC = 0; /** * If possible, seek to a sync sample at or after the specified time */ public static final int SEEK_TO_NEXT_SYNC = 1; /** * If possible, seek to the sync sample closest to the specified time */ public static final int SEEK_TO_CLOSEST_SYNC = 2; /** @hide */ @IntDef({ SEEK_TO_PREVIOUS_SYNC, SEEK_TO_NEXT_SYNC, SEEK_TO_CLOSEST_SYNC, }) @Retention(RetentionPolicy.SOURCE) public @interface SeekMode { } /** * All selected tracks seek near the requested time according to the * specified mode. */ public native void seekTo(long timeUs, @SeekMode int mode); /** * Advance to the next sample. Returns false if no more sample data * is available (end of stream). * * When extracting a local file, the behaviors of {@link #advance} and * {@link #readSampleData} are undefined in presence of concurrent * writes to the same local file; more specifically, end of stream * could be signalled earlier than expected. */ public native boolean advance(); /** * Retrieve the current encoded sample and store it in the byte buffer * starting at the given offset. * <p> * <b>Note:</b>As of API 21, on success the position and limit of * {@code byteBuf} is updated to point to the data just read. * @param byteBuf the destination byte buffer * @return the sample size (or -1 if no more samples are available). */ public native int readSampleData(@NonNull ByteBuffer byteBuf, int offset); /** * Returns the track index the current sample originates from (or -1 * if no more samples are available) */ public native int getSampleTrackIndex(); /** * Returns the current sample's presentation time in microseconds. * or -1 if no more samples are available. */ public native long getSampleTime(); /** * @return size of the current sample in bytes or -1 if no more * samples are available. */ public native long getSampleSize(); // Keep these in sync with their equivalents in NuMediaExtractor.h /** * The sample is a sync sample (or in {@link MediaCodec}'s terminology * it is a key frame.) * * @see MediaCodec#BUFFER_FLAG_KEY_FRAME */ public static final int SAMPLE_FLAG_SYNC = 1; /** * The sample is (at least partially) encrypted, see also the documentation * for {@link android.media.MediaCodec#queueSecureInputBuffer} */ public static final int SAMPLE_FLAG_ENCRYPTED = 2; /** * This indicates that the buffer only contains part of a frame, * and the decoder should batch the data until a buffer without * this flag appears before decoding the frame. * * @see MediaCodec#BUFFER_FLAG_PARTIAL_FRAME */ public static final int SAMPLE_FLAG_PARTIAL_FRAME = 4; /** @hide */ @IntDef(flag = true, value = { SAMPLE_FLAG_SYNC, SAMPLE_FLAG_ENCRYPTED, SAMPLE_FLAG_PARTIAL_FRAME, }) @Retention(RetentionPolicy.SOURCE) public @interface SampleFlag { } /** * Returns the current sample's flags. */ @SampleFlag public native int getSampleFlags(); /** * If the sample flags indicate that the current sample is at least * partially encrypted, this call returns relevant information about * the structure of the sample data required for decryption. * @param info The android.media.MediaCodec.CryptoInfo structure * to be filled in. * @return true iff the sample flags contain {@link #SAMPLE_FLAG_ENCRYPTED} */ public native boolean getSampleCryptoInfo(@NonNull MediaCodec.CryptoInfo info); /** * Returns an estimate of how much data is presently cached in memory * expressed in microseconds. Returns -1 if that information is unavailable * or not applicable (no cache). */ public native long getCachedDuration(); /** * Returns true iff we are caching data and the cache has reached the * end of the data stream (for now, a future seek may of course restart * the fetching of data). * This API only returns a meaningful result if {@link #getCachedDuration} * indicates the presence of a cache, i.e. does NOT return -1. */ public native boolean hasCacheReachedEndOfStream(); /** * Return Metrics data about the current media container. * * @return a {@link PersistableBundle} containing the set of attributes and values * available for the media container being handled by this instance * of MediaExtractor. * The attributes are descibed in {@link MetricsConstants}. * * Additional vendor-specific fields may also be present in * the return value. */ public PersistableBundle getMetrics() { PersistableBundle bundle = native_getMetrics(); return bundle; } private native PersistableBundle native_getMetrics(); private static native final void native_init(); private native final void native_setup(); private native final void native_finalize(); static { System.loadLibrary("media_jni"); native_init(); } private MediaCas mMediaCas; private long mNativeContext; public final static class MetricsConstants { private MetricsConstants() { } /** * Key to extract the container format * from the {@link MediaExtractor#getMetrics} return value. * The value is a String. */ public static final String FORMAT = "android.media.mediaextractor.fmt"; /** * Key to extract the container MIME type * from the {@link MediaExtractor#getMetrics} return value. * The value is a String. */ public static final String MIME_TYPE = "android.media.mediaextractor.mime"; /** * Key to extract the number of tracks in the container * from the {@link MediaExtractor#getMetrics} return value. * The value is an integer. */ public static final String TRACKS = "android.media.mediaextractor.ntrk"; } }