/* * Copyright (C) 2006 The Android Open Source Project * Copyright (C) 2013 YIXIA.COM * * 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 io.vov.vitamio; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.graphics.Canvas; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.text.TextUtils; import android.util.SparseArray; import android.view.Surface; import android.view.SurfaceHolder; import io.vov.vitamio.utils.FileUtils; import io.vov.vitamio.utils.Log; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; /** * MediaPlayer class can be used to control playback of audio/video files and * streams. An example on how to use the methods in this class can be found in * {@link io.vov.vitamio.widget.VideoView}. This class will function the same as * android.media.MediaPlayer in most cases. Please see <a * href="http://developer.android.com/guide/topics/media/index.html">Audio and * Video</a> for additional help using MediaPlayer. */ public class MediaPlayer { public static final int CACHE_TYPE_NOT_AVAILABLE = 1; public static final int CACHE_TYPE_START = 2; public static final int CACHE_TYPE_UPDATE = 3; public static final int CACHE_TYPE_SPEED = 4; public static final int CACHE_TYPE_COMPLETE = 5; public static final int CACHE_INFO_NO_SPACE = 1; public static final int CACHE_INFO_STREAM_NOT_SUPPORT = 2; public static final int MEDIA_ERROR_UNKNOWN = 1; public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200; /** File or network related operation errors. */ public static final int MEDIA_ERROR_IO = -5; /** Bitstream is not conforming to the related coding standard or file spec. */ public static final int MEDIA_ERROR_MALFORMED = -1007; /** Bitstream is conforming to the related coding standard or file spec, but * the media framework does not support the feature. */ public static final int MEDIA_ERROR_UNSUPPORTED = -1010; /** Some operation takes too long to complete, usually more than 3-5 seconds. */ public static final int MEDIA_ERROR_TIMED_OUT = -110; /** * The video is too complex for the decoder: it can't decode frames fast * enough. Possibly only the audio plays fine at this stage. * * @see io.vov.vitamio.MediaPlayer.OnInfoListener */ public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; /** * MediaPlayer is temporarily pausing playback internally in order to buffer * more data. */ public static final int MEDIA_INFO_BUFFERING_START = 701; /** * MediaPlayer is resuming playback after filling buffers. */ public static final int MEDIA_INFO_BUFFERING_END = 702; /** * The media cannot be seeked (e.g live stream) * * @see io.vov.vitamio.MediaPlayer.OnInfoListener */ public static final int MEDIA_INFO_NOT_SEEKABLE = 801; /** * The rate in KB/s of av_read_frame() * * @see io.vov.vitamio.MediaPlayer.OnInfoListener */ public static final int MEDIA_INFO_DOWNLOAD_RATE_CHANGED = 901; public static final int VIDEOQUALITY_LOW = -16; public static final int VIDEOQUALITY_MEDIUM = 0; public static final int VIDEOQUALITY_HIGH = 16; public static final int VIDEOCHROMA_RGB565 = 0; public static final int VIDEOCHROMA_RGBA = 1; /** * The subtitle displayed is embeded in the movie */ public static final int SUBTITLE_INTERNAL = 0; /** * The subtitle displayed is an external file */ public static final int SUBTITLE_EXTERNAL = 1; /** * The external subtitle types which Vitamio supports. */ public static final String[] SUB_TYPES = {".srt", ".ssa", ".smi", ".txt", ".sub", ".ass", ".webvtt"}; private static final int MEDIA_NOP = 0; private static final int MEDIA_PREPARED = 1; private static final int MEDIA_PLAYBACK_COMPLETE = 2; private static final int MEDIA_BUFFERING_UPDATE = 3; private static final int MEDIA_SEEK_COMPLETE = 4; private static final int MEDIA_SET_VIDEO_SIZE = 5; private static final int MEDIA_ERROR = 100; private static final int MEDIA_INFO = 200; private static final int MEDIA_CACHE = 300; private static final int MEDIA_HW_ERROR = 400; private static final int MEDIA_TIMED_TEXT = 1000; private static final int MEDIA_CACHING_UPDATE = 2000; private static final String MEDIA_CACHING_SEGMENTS = "caching_segment"; private static final String MEDIA_CACHING_TYPE = "caching_type"; private static final String MEDIA_CACHING_INFO = "caching_info"; private static final String MEDIA_SUBTITLE_STRING = "sub_string"; private static final String MEDIA_SUBTITLE_BYTES = "sub_bytes"; private static final String MEDIA_SUBTITLE_TYPE = "sub_type"; private static final int SUBTITLE_TEXT = 0; private static final int SUBTITLE_BITMAP = 1; private static AtomicBoolean NATIVE_OMX_LOADED = new AtomicBoolean(false); private Context mContext; private Surface mSurface; private SurfaceHolder mSurfaceHolder; private EventHandler mEventHandler; private PowerManager.WakeLock mWakeLock = null; private boolean mScreenOnWhilePlaying; private boolean mStayAwake; private Metadata mMeta; private TrackInfo[] mInbandTracks; private TrackInfo mOutOfBandTracks; private AssetFileDescriptor mFD = null; private OnHWRenderFailedListener mOnHWRenderFailedListener; private OnPreparedListener mOnPreparedListener; private OnCompletionListener mOnCompletionListener; private OnBufferingUpdateListener mOnBufferingUpdateListener; private OnCachingUpdateListener mOnCachingUpdateListener; private OnSeekCompleteListener mOnSeekCompleteListener; private OnVideoSizeChangedListener mOnVideoSizeChangedListener; private OnErrorListener mOnErrorListener; /** * Register a callback to be invoked when an info/warning is available. * * @param listener * the callback that will be run */ private OnInfoListener mOnInfoListener; private OnTimedTextListener mOnTimedTextListener; private AudioTrack mAudioTrack; private int mAudioTrackBufferSize; private Surface mLocalSurface; private Bitmap mBitmap; private ByteBuffer mByteBuffer; /** * Default constructor. The same as Android's MediaPlayer(). * <p> * When done with the MediaPlayer, you should call {@link #release()}, to free * the resources. If not released, too many MediaPlayer instances may result * in an exception. * </p> */ public MediaPlayer(Context ctx) { this(ctx, false); } /** * Default constructor. The same as Android's MediaPlayer(). * <p> * When done with the MediaPlayer, you should call {@link #release()}, to free * the resources. If not released, too many MediaPlayer instances may result * in an exception. * </p> * * @param preferHWDecoder MediaPlayer will try to use hardware accelerated decoder if true */ public MediaPlayer(Context ctx, boolean preferHWDecoder) { mContext = ctx; String LIB_ROOT = Vitamio.getLibraryPath(); if (preferHWDecoder) { if (!NATIVE_OMX_LOADED.get()) { if (Build.VERSION.SDK_INT > 17) loadOMX_native(LIB_ROOT + "libOMX.18.so"); else if (Build.VERSION.SDK_INT > 13) loadOMX_native(LIB_ROOT + "libOMX.14.so"); else if (Build.VERSION.SDK_INT > 10) loadOMX_native(LIB_ROOT + "libOMX.11.so"); else loadOMX_native(LIB_ROOT + "libOMX.9.so"); NATIVE_OMX_LOADED.set(true); } } else { try { unloadOMX_native(); } catch (UnsatisfiedLinkError e) { Log.e("unloadOMX failed %s", e.toString()); } NATIVE_OMX_LOADED.set(false); } Looper looper; if ((looper = Looper.myLooper()) != null) mEventHandler = new EventHandler(this, looper); else if ((looper = Looper.getMainLooper()) != null) mEventHandler = new EventHandler(this, looper); else mEventHandler = null; native_init(); } static { String LIB_ROOT = Vitamio.getLibraryPath(); try { Log.i("LIB ROOT: %s", LIB_ROOT); System.load(LIB_ROOT + "libstlport_shared.so"); System.load(LIB_ROOT + "libvplayer.so"); loadFFmpeg_native(LIB_ROOT + "libffmpeg.so"); boolean vvo_loaded = false; if (Build.VERSION.SDK_INT > 8) vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.9.so"); else if (Build.VERSION.SDK_INT > 7) vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.8.so"); else vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.7.so"); if (!vvo_loaded) { vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.j.so"); Log.d("FALLBACK TO VVO JNI " + vvo_loaded); } loadVAO_native(LIB_ROOT + "libvao.0.so"); } catch (java.lang.UnsatisfiedLinkError e) { Log.e("Error loading libs", e); } } private static void postEventFromNative(Object mediaplayer_ref, int what, int arg1, int arg2, Object obj) { MediaPlayer mp = (MediaPlayer) (mediaplayer_ref); if (mp == null) return; if (mp.mEventHandler != null) { Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj); mp.mEventHandler.sendMessage(m); } } private static native boolean loadVAO_native(String vaoPath); private static native boolean loadVVO_native(String vvoPath); private static native boolean loadOMX_native(String omxPath); private static native void unloadOMX_native(); private static native boolean loadFFmpeg_native(String ffmpegPath); private native void _setVideoSurface(Surface surface); /** * Sets the SurfaceHolder to use for displaying the video portion of the * media. This call is optional. Not calling it when playing back a video will * result in only the audio track being played. * * @param sh the SurfaceHolder to use for video display */ public void setDisplay(SurfaceHolder sh) { if (sh == null) { releaseDisplay(); } else { mSurfaceHolder = sh; mSurface = sh.getSurface(); _setVideoSurface(mSurface); updateSurfaceScreenOn(); } } /** * Sets the Surface to use for displaying the video portion of the media. This * is similar to {@link #setDisplay(SurfaceHolder)}. * * @param surface the Surface to use for video display */ public void setSurface(Surface surface) { if (surface == null) { releaseDisplay(); } else { mSurfaceHolder = null; mSurface = surface; _setVideoSurface(mSurface); updateSurfaceScreenOn(); } } /** * Sets the data source (file-path or http/rtsp URL) to use. * * @param path the path of the file, or the http/rtsp URL of the stream you want * to play * @throws IllegalStateException if it is called in an invalid state * <p/> * <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)}. */ public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { _setDataSource(path, null, null); } /** * 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 play * @throws IllegalStateException if it is called in an invalid state */ public void setDataSource(Context context, Uri uri) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { setDataSource(context, uri, null); } public void setDataSource(Context context, Uri uri, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { if (context == null || uri == null) throw new IllegalArgumentException(); String scheme = uri.getScheme(); if (scheme == null || scheme.equals("file")) { setDataSource(FileUtils.getPath(uri.toString())); return; } try { ContentResolver resolver = context.getContentResolver(); mFD = resolver.openAssetFileDescriptor(uri, "r"); if (mFD == null) return; setDataSource(mFD.getParcelFileDescriptor().getFileDescriptor()); return; } catch (Exception e) { closeFD(); } setDataSource(uri.toString(), headers); } /** * Sets the data source (file-path or http/rtsp URL) to use. * * @param path the path of the file, or the http/rtsp URL of the stream you want to play * @param headers the headers associated with the http request for the stream you want to play * @throws IllegalStateException if it is called in an invalid state */ public void setDataSource(String path, Map<String, String> headers) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { 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; } } setDataSource(path, keys, values); } /** * Sets the data source (file-path or http/rtsp URL) to use. * * @param path the path of the file, or the http/rtsp URL of the stream you want to play * @param keys AVOption key * @param values AVOption value * @throws IllegalStateException if it is called in an invalid state */ public void setDataSource(String path, String[] keys, String[] values) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { final Uri uri = Uri.parse(path); if ("file".equals(uri.getScheme())) { path = uri.getPath(); } final File file = new File(path); if (file.exists()) { FileInputStream is = new FileInputStream(file); FileDescriptor fd = is.getFD(); setDataSource(fd); is.close(); } else { _setDataSource(path, keys, values); } } /** * Set the segments source url * @param segments the array path of the url e.g. Segmented video list * @param cacheDir e.g. getCacheDir().toString() */ public void setDataSegments(String[] uris, String cacheDir) { _setDataSegmentsSource(uris, cacheDir); } public void setOnHWRenderFailedListener(OnHWRenderFailedListener l) { mOnHWRenderFailedListener = l; } /** * Sets the data source (file-path or http/rtsp/mms URL) to use. * * @param path the path of the file, or the http/rtsp/mms URL of the stream you * want to play * @param keys AVOption key * @param values AVOption value * @throws IllegalStateException if it is called in an invalid state */ private native void _setDataSource(String path, String[] keys, String[] values) throws IOException, IllegalArgumentException, IllegalStateException; /** * 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 play * @throws IllegalStateException if it is called in an invalid state */ public native void setDataSource(FileDescriptor fd) throws IOException, IllegalArgumentException, IllegalStateException; /** * Set the segments source url * @param segments the array path of the url * @param cacheDir e.g. getCacheDir().toString() */ private native void _setDataSegmentsSource(String[] segments, String cacheDir); /** * Prepares the player for playback, synchronously. * <p/> * After setting the datasource and the display surface, you need to either * call prepare() or prepareAsync(). For files, it is OK to call prepare(), * which blocks until MediaPlayer is ready for playback. * * @throws IllegalStateException if it is called in an invalid state */ public native void prepare() throws IOException, IllegalStateException; /** * Prepares the player for playback, asynchronously. * <p/> * After setting the datasource and the display surface, you need to either * call prepare() or prepareAsync(). For streams, you should call * prepareAsync(), which returns immediately, rather than blocking until * enough data has been buffered. * * @throws IllegalStateException if it is called in an invalid state */ public native void prepareAsync() throws IllegalStateException; /** * Starts or resumes playback. If playback had previously been paused, * playback will continue from where it was paused. If playback had been * stopped, or never started before, playback will start at the beginning. * * @throws IllegalStateException if it is called in an invalid state */ public void start() throws IllegalStateException { stayAwake(true); _start(); } private native void _start() throws IllegalStateException; /** * The same as {@link #pause()} * * @throws IllegalStateException if the internal player engine has not been initialized. */ public void stop() throws IllegalStateException { stayAwake(false); _stop(); } private native void _stop() throws IllegalStateException; /** * Pauses playback. Call start() to resume. * * @throws IllegalStateException if the internal player engine has not been initialized. */ public void pause() throws IllegalStateException { stayAwake(false); _pause(); } private native void _pause() throws IllegalStateException; /** * Set the low-level power management behavior for this MediaPlayer. This can * be used when the MediaPlayer is not playing through a SurfaceHolder set * with {@link #setDisplay(SurfaceHolder)} and thus can use the high-level * {@link #setScreenOnWhilePlaying(boolean)} feature. * <p/> * This function has the MediaPlayer access the low-level power manager * service to control the device's power usage while playing is occurring. The * parameter is a combination of {@link android.os.PowerManager} wake flags. * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK} * permission. By default, no attempt is made to keep the device awake during * playback. * * @param context the Context to use * @param mode the power/wake mode to set * @see android.os.PowerManager */ @SuppressLint("Wakelock") public void setWakeMode(Context context, int mode) { boolean washeld = false; if (mWakeLock != null) { if (mWakeLock.isHeld()) { washeld = true; mWakeLock.release(); } mWakeLock = null; } PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(mode | PowerManager.ON_AFTER_RELEASE, MediaPlayer.class.getName()); mWakeLock.setReferenceCounted(false); if (washeld) { mWakeLock.acquire(); } } /** * Control whether we should use the attached SurfaceHolder to keep the screen * on while video playback is occurring. This is the preferred method over * {@link #setWakeMode} where possible, since it doesn't require that the * application have permission for low-level wake lock access. * * @param screenOn Supply true to keep the screen on, false to allow it to turn off. */ public void setScreenOnWhilePlaying(boolean screenOn) { if (mScreenOnWhilePlaying != screenOn) { mScreenOnWhilePlaying = screenOn; updateSurfaceScreenOn(); } } @SuppressLint("Wakelock") private void stayAwake(boolean awake) { if (mWakeLock != null) { if (awake && !mWakeLock.isHeld()) { mWakeLock.acquire(); } else if (!awake && mWakeLock.isHeld()) { mWakeLock.release(); } } mStayAwake = awake; updateSurfaceScreenOn(); } private void updateSurfaceScreenOn() { if (mSurfaceHolder != null) mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake); } /** * Returns the width of the video. * * @return the width of the video, or 0 if there is no video, or the width has * not been determined yet. The OnVideoSizeChangedListener can be * registered via * {@link #setOnVideoSizeChangedListener(OnVideoSizeChangedListener)} * to provide a notification when the width is available. */ public native int getVideoWidth(); private native int getVideoWidth_a(); /** * Returns the height of the video. * * @return the height of the video, or 0 if there is no video, or the height * has not been determined yet. The OnVideoSizeChangedListener can be * registered via * {@link #setOnVideoSizeChangedListener(OnVideoSizeChangedListener)} * to provide a notification when the height is available. */ public native int getVideoHeight(); private native int getVideoHeight_a(); /** * Checks whether the MediaPlayer is playing. * * @return true if currently playing, false otherwise */ public native boolean isPlaying(); /** * Set whether cache the online playback file * @param cache */ public native void setUseCache(boolean cache); /** * set cache file dir * @param directory */ public native void setCacheDirectory(String directory); /** * Adaptive streaming support, default is false * * @param adaptive true if wanna adaptive steam * */ public native void setAdaptiveStream(boolean adaptive); /** * Seeks to specified time position. * * @param msec the offset in milliseconds from the start to seek to * @throws IllegalStateException if the internal player engine has not been initialized */ public native void seekTo(long msec) throws IllegalStateException; /** * Gets the current playback position. * * @return the current position in milliseconds */ public native long getCurrentPosition(); /** * Get the current video frame * * @return bitmap object */ public native Bitmap getCurrentFrame(); /** * Gets the duration of the file. * * @return the duration in milliseconds */ public native long getDuration(); /** * Gets the media metadata. * * @return The metadata, possibly empty. null if an error occurred. */ public Metadata getMetadata() { if (mMeta == null) { mMeta = new Metadata(); Map<byte[], byte[]> meta = new HashMap<byte[], byte[]>(); if (!native_getMetadata(meta)) { return null; } if (!mMeta.parse(meta, getMetaEncoding())) { return null; } } return mMeta; } /** * Releases resources associated with this MediaPlayer object. It is * considered good practice to call this method when you're done using the * MediaPlayer. */ public void release() { stayAwake(false); updateSurfaceScreenOn(); mOnPreparedListener = null; mOnBufferingUpdateListener = null; mOnCompletionListener = null; mOnSeekCompleteListener = null; mOnErrorListener = null; mOnInfoListener = null; mOnVideoSizeChangedListener = null; mOnCachingUpdateListener = null; mOnHWRenderFailedListener = null; _release(); closeFD(); } private native void _release(); /** * Resets the MediaPlayer to its uninitialized state. After calling this * method, you will have to initialize it again by setting the data source and * calling prepare(). */ public void reset() { stayAwake(false); _reset(); mEventHandler.removeCallbacksAndMessages(null); closeFD(); } private native void _reset(); private void closeFD() { if (mFD != null) { try { mFD.close(); } catch (IOException e) { Log.e("closeFD", e); } mFD = null; } } /** * Sets the player to be looping or non-looping. * * @param looping whether to loop or not */ public native void setLooping(boolean looping); /** * Checks whether the MediaPlayer is looping or non-looping. * * @return true if the MediaPlayer is currently looping, false otherwise */ public native boolean isLooping(); /** * Amplify audio * * @param ratio e.g. 3.5 */ public native void setAudioAmplify(float ratio); public native void setVolume(float leftVolume, float rightVolume); private native final boolean native_getTrackInfo(SparseArray<byte[]> trackSparse); private native final boolean native_getMetadata(Map<byte[], byte[]> meta); private native final void native_init(); private native final void native_finalize(); /** * Returns an array of track information. * * @return Array of track info. The total number of tracks is the array * length. Must be called again if an external timed text source has * been added after any of the addTimedTextSource methods are called. */ public TrackInfo[] getTrackInfo(String encoding) { TrackInfo[] trackInfo = getInbandTrackInfo(encoding); // add out-of-band tracks String timedTextPath = getTimedTextPath(); if (TextUtils.isEmpty(timedTextPath)) { return trackInfo; } TrackInfo[] allTrackInfo = new TrackInfo[trackInfo.length + 1]; System.arraycopy(trackInfo, 0, allTrackInfo, 0, trackInfo.length); int i = trackInfo.length; SparseArray<MediaFormat> trackInfoArray = new SparseArray<MediaFormat>(); MediaFormat mediaFormat = new MediaFormat(); mediaFormat.setString(MediaFormat.KEY_TITLE, timedTextPath.substring(timedTextPath.lastIndexOf("/"))); mediaFormat.setString(MediaFormat.KEY_PATH, timedTextPath); SparseArray<MediaFormat> timedTextSparse = findTrackFromTrackInfo(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT, trackInfo); if (timedTextSparse == null || timedTextSparse.size() == 0) trackInfoArray.put(timedTextSparse.keyAt(0), mediaFormat); else trackInfoArray.put(timedTextSparse.keyAt(timedTextSparse.size() - 1), mediaFormat); mOutOfBandTracks = new TrackInfo(TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, trackInfoArray); allTrackInfo[i] = mOutOfBandTracks; return allTrackInfo; } private TrackInfo[] getInbandTrackInfo(String encoding) { if (mInbandTracks == null) { SparseArray<byte[]> trackSparse = new SparseArray<byte[]>(); if (!native_getTrackInfo(trackSparse)) { return null; } int size = trackSparse.size(); mInbandTracks = new TrackInfo[size]; for (int i = 0; i < size; i++) { SparseArray<MediaFormat> sparseArray = parseTrackInfo(trackSparse.valueAt(i), encoding); TrackInfo trackInfo = new TrackInfo(trackSparse.keyAt(i), sparseArray); mInbandTracks[i] = trackInfo; } } return mInbandTracks; } /** * Use default chartset {@link #getTrackInfo()} method. * * @return array of {@link TrackInfo} */ public TrackInfo[] getTrackInfo() { return getTrackInfo(Charset.defaultCharset().name()); } private SparseArray<MediaFormat> parseTrackInfo(byte[] tracks, String encoding) { SparseArray<MediaFormat> trackSparse = new SparseArray<MediaFormat>(); String trackString; int trackNum; try { trackString = new String(tracks, encoding); } catch (Exception e) { Log.e("getTrackMap exception"); trackString = new String(tracks); } for (String s : trackString.split("!#!")) { try { MediaFormat mediaFormat = null; String[] formats = s.split("\\."); if (formats == null) continue; trackNum = Integer.parseInt(formats[0]); if (formats.length == 3) { mediaFormat = MediaFormat.createSubtitleFormat(formats[2], formats[1]); } else if (formats.length == 2) { mediaFormat = MediaFormat.createSubtitleFormat("", formats[1]); } trackSparse.put(trackNum, mediaFormat); } catch (NumberFormatException e) { } } return trackSparse; } /** * @param mediaTrackType * @param trackInfo * @return {@link TrackInfo#getTrackInfoArray()} */ public SparseArray<MediaFormat> findTrackFromTrackInfo(int mediaTrackType, TrackInfo[] trackInfo) { for (int i = 0; i < trackInfo.length; i++) { if (trackInfo[i].getTrackType() == mediaTrackType) { return trackInfo[i].getTrackInfoArray(); } } return null; } /** * Set the file-path of an external timed text. * * @param path must be a local file */ public native void addTimedTextSource(String path); /** * Selects a track. * <p> * In any valid state, if it is called multiple times on the same type of * track (ie. Video, Audio, Timed Text), the most recent one will be chosen. * </p> * <p> * The first audio and video tracks are selected by default if available, even * though this method is not called. However, no timed text track will be * selected until this function is called. * </p> * * @param index the index of the track to be selected. The valid range of the * index is 0..total number of track - 1. The total number of tracks * as well as the type of each individual track can be found by * calling {@link #getTrackInfo()} method. * @see io.vov.vitamio.MediaPlayer#getTrackInfo */ public void selectTrack(int index) { selectOrDeselectBandTrack(index, true /* select */); } /** * Deselect a track. * <p> * Currently, the track must be a timed text track and no audio or video * tracks can be deselected. * </p> * * @param index the index of the track to be deselected. The valid range of the * index is 0..total number of tracks - 1. The total number of tracks * as well as the type of each individual track can be found by * calling {@link #getTrackInfo()} method. * @see io.vov.vitamio.MediaPlayer#getTrackInfo */ public void deselectTrack(int index) { selectOrDeselectBandTrack(index, false /* select */); } private void selectOrDeselectBandTrack(int index, boolean select) { if (mOutOfBandTracks != null) { SparseArray<MediaFormat> mediaSparse = mOutOfBandTracks.getTrackInfoArray(); int trackIndex = mediaSparse.keyAt(0); MediaFormat mediaFormat = mediaSparse.valueAt(0); if (index == trackIndex && select) { addTimedTextSource(mediaFormat.getString(MediaFormat.KEY_PATH)); return; } } selectOrDeselectTrack(index, select); } private native void selectOrDeselectTrack(int index, boolean select); @Override protected void finalize() { native_finalize(); } /** * Register a callback to be invoked when the media source is ready for * playback. * * @param listener the callback that will be run */ public void setOnPreparedListener(OnPreparedListener listener) { mOnPreparedListener = listener; } /** * Register a callback to be invoked when the end of a media source has been * reached during playback. * * @param listener the callback that will be run */ public void setOnCompletionListener(OnCompletionListener listener) { mOnCompletionListener = listener; } /** * Register a callback to be invoked when the status of a network stream's * buffer has changed. * * @param listener the callback that will be run. */ public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) { mOnBufferingUpdateListener = listener; } /** * Register a callback to be invoked when the segments cached on storage has * changed. * * @param listener the callback that will be run. */ public void setOnCachingUpdateListener(OnCachingUpdateListener listener) { mOnCachingUpdateListener = listener; } private void updateCacheStatus(int type, int info, long[] segments) { if (mEventHandler != null) { Message m = mEventHandler.obtainMessage(MEDIA_CACHING_UPDATE); Bundle b = m.getData(); b.putInt(MEDIA_CACHING_TYPE, type); b.putInt(MEDIA_CACHING_INFO, info); b.putLongArray(MEDIA_CACHING_SEGMENTS, segments); mEventHandler.sendMessage(m); } } /** * Register a callback to be invoked when a seek operation has been completed. * * @param listener the callback that will be run */ public void setOnSeekCompleteListener(OnSeekCompleteListener listener) { mOnSeekCompleteListener = listener; } /** * Register a callback to be invoked when the video size is known or updated. * * @param listener the callback that will be run */ public void setOnVideoSizeChangedListener(OnVideoSizeChangedListener listener) { mOnVideoSizeChangedListener = listener; } /** * Register a callback to be invoked when an error has happened during an * asynchronous operation. * * @param listener the callback that will be run */ public void setOnErrorListener(OnErrorListener listener) { mOnErrorListener = listener; } public void setOnInfoListener(OnInfoListener listener) { mOnInfoListener = listener; } /** * Register a callback to be invoked when a timed text need to display. * * @param listener the callback that will be run */ public void setOnTimedTextListener(OnTimedTextListener listener) { mOnTimedTextListener = listener; } private void updateSub(int subType, byte[] bytes, String encoding, int width, int height) { if (mEventHandler != null) { Message m = mEventHandler.obtainMessage(MEDIA_TIMED_TEXT, width, height); Bundle b = m.getData(); if (subType == SUBTITLE_TEXT) { b.putInt(MEDIA_SUBTITLE_TYPE, SUBTITLE_TEXT); if (encoding == null) { b.putString(MEDIA_SUBTITLE_STRING, new String(bytes)); } else { try { b.putString(MEDIA_SUBTITLE_STRING, new String(bytes, encoding.trim())); } catch (UnsupportedEncodingException e) { Log.e("updateSub", e); b.putString(MEDIA_SUBTITLE_STRING, new String(bytes)); } } } else if (subType == SUBTITLE_BITMAP) { b.putInt(MEDIA_SUBTITLE_TYPE, SUBTITLE_BITMAP); b.putByteArray(MEDIA_SUBTITLE_BYTES, bytes); } mEventHandler.sendMessage(m); } } protected native void _releaseVideoSurface(); /** * Calling this result in only the audio track being played. */ public void releaseDisplay() { _releaseVideoSurface(); mSurfaceHolder = null; mSurface = null; } /** * Returns the aspect ratio of the video. * * @return the aspect ratio of the video, or 0 if there is no video, or the * width and height is not available. * @see io.vov.vitamio.widget.VideoView#setVideoLayout(int, float) */ public native float getVideoAspectRatio(); /** * Set the quality when play video, if the video is too lag, you may try * VIDEOQUALITY_LOW, default is VIDEOQUALITY_LOW. * * @param quality <ul> * <li>{@link #VIDEOQUALITY_HIGH} * <li>{@link #VIDEOQUALITY_MEDIUM} * <li>{@link #VIDEOQUALITY_LOW} * </ul> */ public native void setVideoQuality(int quality); /** * Set the Video Chroma quality when play video, default is VIDEOCHROMA_RGB565 * You can set on after {@link #prepareAsync()}. * @param chroma <ul> * <li>{@link #VIDEOCHROMA_RGB565} * <li>{@link #VIDEOCHROMA_RGBA} * </ul> */ public native void setVideoChroma(int chroma); /** * Set if should deinterlace the video picture * * @param deinterlace */ public native void setDeinterlace(boolean deinterlace); /** * The buffer to fill before playback, default is 1024*1024 Byte * * @param bufSize buffer size in Byte */ public native void setBufferSize(long bufSize); /** * Set video and audio playback speed * * @param speed e.g. 0.8 or 2.0, default to 1.0, range in [0.5-2] */ public native void setPlaybackSpeed(float speed); /** * Checks whether the buffer is filled * * @return false if buffer is filled */ public native boolean isBuffering(); /** * @return the percent * @see io.vov.vitamio.MediaPlayer.OnBufferingUpdateListener */ public native int getBufferProgress(); /** * Get the encoding if haven't set with {@link #setMetaEncoding(String)} * * @return the encoding */ public native String getMetaEncoding(); /** * Set the encoding MediaPlayer will use to determine the metadata * * @param encoding e.g. "UTF-8" */ public native void setMetaEncoding(String encoding); /** * Get the audio track number in playback * * @return track number */ public native int getAudioTrack(); /** * Get the video track number in playback * * @return track number */ public native int getVideoTrack(); /** * Tell the MediaPlayer whether to show timed text * * @param shown true if wanna show */ public native void setTimedTextShown(boolean shown); /** * Set the encoding to display timed text. * * @param encoding MediaPlayer will detet it if null */ public native void setTimedTextEncoding(String encoding); /** * @return <ul> * <li>{@link #SUBTITLE_EXTERNAL} * <li>{@link #SUBTITLE_INTERNAL} * </ul> */ public native int getTimedTextLocation(); /** * You can get the file-path of the external subtitle in use. * * @return null if no external subtitle */ public native String getTimedTextPath(); /** * Get the subtitle track number in playback * * @return track number */ public native int getTimedTextTrack(); private int audioTrackInit(int sampleRateInHz, int channels) { audioTrackRelease(); int channelConfig = channels >= 2 ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO; try { mAudioTrackBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT); mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT, mAudioTrackBufferSize, AudioTrack.MODE_STREAM); } catch (Exception e) { mAudioTrackBufferSize = 0; Log.e("audioTrackInit", e); } return mAudioTrackBufferSize; } private void audioTrackSetVolume(float leftVolume, float rightVolume) { if (mAudioTrack != null) mAudioTrack.setStereoVolume(leftVolume, rightVolume); } private void audioTrackWrite(byte[] audioData, int offsetInBytes, int sizeInBytes) { if (mAudioTrack != null && mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { int written; while (sizeInBytes > 0) { written = sizeInBytes > mAudioTrackBufferSize ? mAudioTrackBufferSize : sizeInBytes; mAudioTrack.write(audioData, offsetInBytes, written); sizeInBytes -= written; offsetInBytes += written; } } } private void audioTrackStart() { if (mAudioTrack != null && mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED && mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) mAudioTrack.play(); } private void audioTrackPause() { if (mAudioTrack != null && mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) mAudioTrack.pause(); } private void audioTrackRelease() { if (mAudioTrack != null) { if (mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) mAudioTrack.stop(); mAudioTrack.release(); } mAudioTrack = null; } public int getAudioSessionId() { return mAudioTrack.getAudioSessionId(); } private ByteBuffer surfaceInit() { synchronized (this) { mLocalSurface = mSurface; int w = getVideoWidth_a(); int h = getVideoHeight_a(); if (mLocalSurface != null && w != 0 && h != 0) { mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565); mByteBuffer = ByteBuffer.allocateDirect(w * h * 2); } else { mBitmap = null; mByteBuffer = null; } return mByteBuffer; } } private void surfaceRender() { synchronized (this) { if (mLocalSurface == null || !mLocalSurface.isValid() || mBitmap == null || mByteBuffer == null) return; try { Canvas c = mLocalSurface.lockCanvas(null); mBitmap.copyPixelsFromBuffer(mByteBuffer); c.drawBitmap(mBitmap, 0, 0, null); mLocalSurface.unlockCanvasAndPost(c); } catch (Exception e) { Log.e("surfaceRender", e); } } } private void surfaceRelease() { synchronized (this) { mLocalSurface = null; mBitmap = null; mByteBuffer = null; } } public interface OnHWRenderFailedListener { public void onFailed(); } public interface OnPreparedListener { /** * Called when the media file is ready for playback. * * @param mp the MediaPlayer that is ready for playback */ void onPrepared(MediaPlayer mp); } public interface OnCompletionListener { /** * Called when the end of a media source is reached during playback. * * @param mp the MediaPlayer that reached the end of the file */ void onCompletion(MediaPlayer mp); } public interface OnBufferingUpdateListener { /** * Called to update status in buffering a media stream. Buffering is storing * data in memory while caching on external storage. * * @param mp the MediaPlayer the update pertains to * @param percent the percentage (0-100) of the buffer that has been filled thus * far */ void onBufferingUpdate(MediaPlayer mp, int percent); } public interface OnCachingUpdateListener { /** * Called to update status in caching a media stream. Caching is storing * data on external storage while buffering in memory. * * @param mp the MediaPlayer the update pertains to * @param segments the cached segments in bytes, in format [s1begin, s1end, * s2begin, s2end], s1begin < s1end < s2begin < s2end. e.g. [124, * 100423, 4321412, 214323433] */ void onCachingUpdate(MediaPlayer mp, long[] segments); /** * Cache speed * * @param mp the MediaPlayer the update pertains to * @param speed the cached speed size kb/s */ void onCachingSpeed(MediaPlayer mp, int speed); /** * Cache start * @param mp */ void onCachingStart(MediaPlayer mp); /** * Cache compelete */ void onCachingComplete(MediaPlayer mp); /** * Cache not available * * @param mp the MediaPlayer the update pertains to * @param info the not available info * <ul> * <li>{@link #CACHE_INFO_NO_SPACE} * <li>{@link #CACHE_INFO_STREAM_NOT_SUPPORT} * </ul> */ void onCachingNotAvailable(MediaPlayer mp, int info); } public interface OnSeekCompleteListener { /** * Called to indicate the completion of a seek operation. * * @param mp the MediaPlayer that issued the seek operation */ public void onSeekComplete(MediaPlayer mp); } public interface OnVideoSizeChangedListener { /** * Called to indicate the video size * * @param mp the MediaPlayer associated with this callback * @param width the width of the video * @param height the height of the video */ public void onVideoSizeChanged(MediaPlayer mp, int width, int height); } public interface OnErrorListener { /** * Called to indicate an error. * * @param mp the MediaPlayer the error pertains to * @param what the type of error that has occurred: * <ul> * <li>{@link #MEDIA_ERROR_UNKNOWN} * <li> * {@link #MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK} * </ul> * @param extra an extra code, specific to the error. Typically implementation * dependant. * @return True if the method handled the error, false if it didn't. * Returning false, or not having an OnErrorListener at all, will * cause the OnCompletionListener to be called. */ boolean onError(MediaPlayer mp, int what, int extra); } public interface OnInfoListener { /** * Called to indicate an info or a warning. * * @param mp the MediaPlayer the info pertains to. * @param what the type of info or warning. * <ul> * <li>{@link #MEDIA_INFO_VIDEO_TRACK_LAGGING} * <li>{@link #MEDIA_INFO_BUFFERING_START} * <li>{@link #MEDIA_INFO_BUFFERING_END} * <li>{@link #MEDIA_INFO_NOT_SEEKABLE} * <li>{@link #MEDIA_INFO_DOWNLOAD_RATE_CHANGED} * </ul> * @param extra an extra code, specific to the info. Typically implementation * dependant. * @return True if the method handled the info, false if it didn't. * Returning false, or not having an OnErrorListener at all, will * cause the info to be discarded. */ boolean onInfo(MediaPlayer mp, int what, int extra); } public interface OnTimedTextListener { /** * Called to indicate that a text timed text need to display * * @param text the timedText to display */ public void onTimedText(String text); /** * Called to indicate that an image timed text need to display * * @param pixels the pixels of the timed text image * @param width the width of the timed text image * @param height the height of the timed text image */ public void onTimedTextUpdate(byte[] pixels, int width, int height); } /** * Class for MediaPlayer to return each audio/video/subtitle track's metadata. * * @see io.vov.vitamio.MediaPlayer#getTrackInfo */ static public class TrackInfo { public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; public static final int MEDIA_TRACK_TYPE_VIDEO = 1; public static final int MEDIA_TRACK_TYPE_AUDIO = 2; public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3; public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; final int mTrackType; final SparseArray<MediaFormat> mTrackInfoArray; TrackInfo(int trackType, SparseArray<MediaFormat> trackInfoArray) { mTrackType = trackType; mTrackInfoArray = trackInfoArray; } /** * Gets the track type. * * @return TrackType which indicates if the track is video, audio, timed * text. */ public int getTrackType() { return mTrackType; } /** * Gets the track info * * @return map trackIndex to MediaFormat */ public SparseArray<MediaFormat> getTrackInfoArray() { return mTrackInfoArray; } } @SuppressLint("HandlerLeak") private class EventHandler extends Handler { private MediaPlayer mMediaPlayer; private Bundle mData; public EventHandler(MediaPlayer mp, Looper looper) { super(looper); mMediaPlayer = mp; } @Override public void handleMessage(Message msg) { switch (msg.what) { case MEDIA_PREPARED: if (mOnPreparedListener != null) mOnPreparedListener.onPrepared(mMediaPlayer); return; case MEDIA_PLAYBACK_COMPLETE: if (mOnCompletionListener != null) mOnCompletionListener.onCompletion(mMediaPlayer); stayAwake(false); return; case MEDIA_BUFFERING_UPDATE: if (mOnBufferingUpdateListener != null) mOnBufferingUpdateListener.onBufferingUpdate(mMediaPlayer, msg.arg1); return; case MEDIA_SEEK_COMPLETE: if (isPlaying()) stayAwake(true); if (mOnSeekCompleteListener != null) mOnSeekCompleteListener.onSeekComplete(mMediaPlayer); return; case MEDIA_SET_VIDEO_SIZE: if (mOnVideoSizeChangedListener != null) mOnVideoSizeChangedListener.onVideoSizeChanged(mMediaPlayer, msg.arg1, msg.arg2); return; case MEDIA_ERROR: Log.e("Error (%d, %d)", msg.arg1, msg.arg2); boolean error_was_handled = false; if (mOnErrorListener != null) error_was_handled = mOnErrorListener.onError(mMediaPlayer, msg.arg1, msg.arg2); if (mOnCompletionListener != null && !error_was_handled) mOnCompletionListener.onCompletion(mMediaPlayer); stayAwake(false); return; case MEDIA_INFO: Log.i("Info (%d, %d)", msg.arg1, msg.arg2); if (mOnInfoListener != null) mOnInfoListener.onInfo(mMediaPlayer, msg.arg1, msg.arg2); return; case MEDIA_CACHE: return; case MEDIA_TIMED_TEXT: mData = msg.getData(); if (mData.getInt(MEDIA_SUBTITLE_TYPE) == SUBTITLE_TEXT) { Log.i("Subtitle : %s", mData.getString(MEDIA_SUBTITLE_STRING)); if (mOnTimedTextListener != null) mOnTimedTextListener.onTimedText(mData.getString(MEDIA_SUBTITLE_STRING)); } else if (mData.getInt(MEDIA_SUBTITLE_TYPE) == SUBTITLE_BITMAP) { Log.i("Subtitle : bitmap"); if (mOnTimedTextListener != null) mOnTimedTextListener.onTimedTextUpdate(mData.getByteArray(MEDIA_SUBTITLE_BYTES), msg.arg1, msg.arg2); } return; case MEDIA_CACHING_UPDATE: if (mOnCachingUpdateListener != null) { int cacheType = msg.getData().getInt(MEDIA_CACHING_TYPE); if (cacheType == CACHE_TYPE_NOT_AVAILABLE) { mOnCachingUpdateListener.onCachingNotAvailable(mMediaPlayer, msg.getData().getInt(MEDIA_CACHING_INFO)); } else if (cacheType == CACHE_TYPE_UPDATE) { mOnCachingUpdateListener.onCachingUpdate(mMediaPlayer, msg.getData().getLongArray(MEDIA_CACHING_SEGMENTS)); } else if (cacheType == CACHE_TYPE_SPEED) { mOnCachingUpdateListener.onCachingSpeed(mMediaPlayer, msg.getData().getInt(MEDIA_CACHING_INFO)); } else if (cacheType == CACHE_TYPE_START) { mOnCachingUpdateListener.onCachingStart(mMediaPlayer); } else if (cacheType == CACHE_TYPE_COMPLETE) { mOnCachingUpdateListener.onCachingComplete(mMediaPlayer); } } return; case MEDIA_NOP: return; case MEDIA_HW_ERROR: if (mOnHWRenderFailedListener != null) mOnHWRenderFailedListener.onFailed(); return; default: Log.e("Unknown message type " + msg.what); return; } } } }