package net.pocketmagic.android.openmxplayer; /* ** OpenMXPlayer - Freeware audio player library for Android ** Copyright (C) 2009 - 2014 Radu Motisan, radu.motisan@gmail.com ** ** This file is a part of "OpenMXPlayer" open source library. ** ** OpenMXPlayer is free software; you can redistribute it and/or modify ** it under the terms of the GNU Lesser General Public License as published ** by the Free Software Foundation; either version 3 of the License, ** or (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU Lesser General Public License for more details. ** ** You should have received a copy of the GNU Lesser General Public License ** along with this program. If not, see <http://www.gnu.org/licenses/>. */ import java.io.FileDescriptor; import java.nio.ByteBuffer; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaExtractor; import android.media.MediaFormat; import android.os.Handler; import android.util.Log; public class OpenMXPlayer implements Runnable { public final String LOG_TAG = "OpenMXPlayer"; private MediaExtractor extractor; private MediaCodec codec; private AudioTrack audioTrack; private PlayerEvents events = null; private PlayerStates state = new PlayerStates(); private String sourcePath = null; private int sourceRawResId = -1; private Context mContext; private boolean stop = false; Handler handler = new Handler(); String mime = null; int sampleRate = 0, channels = 0, bitrate = 0; long presentationTimeUs = 0, duration = 0; public void setEventsListener(PlayerEvents events) { this.events = events; } public OpenMXPlayer() { } public OpenMXPlayer(PlayerEvents events) { setEventsListener(events); } /** * For live streams, duration is 0 * @return */ public boolean isLive() { return (duration == 0); } /** * set the data source, a file path or an url, or a file descriptor, to play encoded audio from * @param src */ public void setDataSource(String src) { sourcePath = src; } public void setDataSource(Context context, int resid) { mContext = context; sourceRawResId = resid; } public void play() { if (state.get() == PlayerStates.STOPPED) { stop = false; new Thread(this).start(); } if (state.get() == PlayerStates.READY_TO_PLAY) { state.set(PlayerStates.PLAYING); syncNotify(); } } /** * Call notify to control the PAUSE (waiting) state, when the state is changed */ public synchronized void syncNotify() { notify(); } public void stop() { stop = true; } public void pause() { state.set(PlayerStates.READY_TO_PLAY); } public void seek(long pos) { extractor.seekTo(pos, MediaExtractor.SEEK_TO_CLOSEST_SYNC); } public void seek(int percent) { long pos = percent * duration / 100; seek(pos); } /** * A pause mechanism that would block current thread when pause flag is set (READY_TO_PLAY) */ public synchronized void waitPlay(){ // if (duration == 0) return; while(state.get() == PlayerStates.READY_TO_PLAY) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); // extractor gets information about the stream extractor = new MediaExtractor(); // try to set the source, this might fail try { if (sourcePath != null) extractor.setDataSource(this.sourcePath); if (sourceRawResId != -1) { AssetFileDescriptor fd = mContext.getResources().openRawResourceFd(sourceRawResId); extractor.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getDeclaredLength()); fd.close(); } } catch (Exception e) { Log.e(LOG_TAG, "exception:"+e.getMessage()); if (events != null) handler.post(new Runnable() { @Override public void run() { events.onError(); } }); return; } // Read track header MediaFormat format = null; try { format = extractor.getTrackFormat(0); mime = format.getString(MediaFormat.KEY_MIME); sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); // if duration is 0, we are probably playing a live stream duration = format.getLong(MediaFormat.KEY_DURATION); bitrate = format.getInteger(MediaFormat.KEY_BIT_RATE); } catch (Exception e) { Log.e(LOG_TAG, "Reading format parameters exception:"+e.getMessage()); // don't exit, tolerate this error, we'll fail later if this is critical } Log.d(LOG_TAG, "Track info: mime:" + mime + " sampleRate:" + sampleRate + " channels:" + channels + " bitrate:" + bitrate + " duration:" + duration); // check we have audio content we know if (format == null || !mime.startsWith("audio/")) { if (events != null) handler.post(new Runnable() { @Override public void run() { events.onError(); } }); return; } // create the actual decoder, using the mime to select codec = MediaCodec.createDecoderByType(mime); // check we have a valid codec instance if (codec == null) { if (events != null) handler.post(new Runnable() { @Override public void run() { events.onError(); } }); return; } //state.set(PlayerStates.READY_TO_PLAY); if (events != null) handler.post(new Runnable() { @Override public void run() { events.onStart(mime, sampleRate, channels, duration); } }); codec.configure(format, null, null, 0); codec.start(); ByteBuffer[] codecInputBuffers = codec.getInputBuffers(); ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers(); // configure AudioTrack int channelConfiguration = channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO; int minSize = AudioTrack.getMinBufferSize( sampleRate, channelConfiguration, AudioFormat.ENCODING_PCM_16BIT); audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfiguration, AudioFormat.ENCODING_PCM_16BIT, minSize, AudioTrack.MODE_STREAM); // start playing, we will feed the AudioTrack later audioTrack.play(); extractor.selectTrack(0); // start decoding final long kTimeOutUs = 1000; MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); boolean sawInputEOS = false; boolean sawOutputEOS = false; int noOutputCounter = 0; int noOutputCounterLimit = 10; state.set(PlayerStates.PLAYING); while (!sawOutputEOS && noOutputCounter < noOutputCounterLimit && !stop) { // pause implementation waitPlay(); noOutputCounter++; // read a buffer before feeding it to the decoder if (!sawInputEOS) { int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs); if (inputBufIndex >= 0) { ByteBuffer dstBuf = codecInputBuffers[inputBufIndex]; int sampleSize = extractor.readSampleData(dstBuf, 0); if (sampleSize < 0) { Log.d(LOG_TAG, "saw input EOS. Stopping playback"); sawInputEOS = true; sampleSize = 0; } else { presentationTimeUs = extractor.getSampleTime(); final int percent = (duration == 0)? 0 : (int) (100 * presentationTimeUs / duration); if (events != null) handler.post(new Runnable() { @Override public void run() { events.onPlayUpdate(percent, presentationTimeUs / 1000, duration / 1000); } }); } codec.queueInputBuffer(inputBufIndex, 0, sampleSize, presentationTimeUs, sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0); if (!sawInputEOS) extractor.advance(); } else { Log.e(LOG_TAG, "inputBufIndex " +inputBufIndex); } } // !sawInputEOS // decode to PCM and push it to the AudioTrack player int res = codec.dequeueOutputBuffer(info, kTimeOutUs); if (res >= 0) { if (info.size > 0) noOutputCounter = 0; int outputBufIndex = res; ByteBuffer buf = codecOutputBuffers[outputBufIndex]; final byte[] chunk = new byte[info.size]; buf.get(chunk); buf.clear(); if(chunk.length > 0){ audioTrack.write(chunk,0,chunk.length); /*if(this.state.get() != PlayerStates.PLAYING) { if (events != null) handler.post(new Runnable() { @Override public void run() { events.onPlay(); } }); state.set(PlayerStates.PLAYING); }*/ } codec.releaseOutputBuffer(outputBufIndex, false); if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { Log.d(LOG_TAG, "saw output EOS."); sawOutputEOS = true; } } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { codecOutputBuffers = codec.getOutputBuffers(); Log.d(LOG_TAG, "output buffers have changed."); } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat oformat = codec.getOutputFormat(); Log.d(LOG_TAG, "output format has changed to " + oformat); } else { Log.d(LOG_TAG, "dequeueOutputBuffer returned " + res); } } Log.d(LOG_TAG, "stopping..."); if(codec != null) { codec.stop(); codec.release(); codec = null; } if(audioTrack != null) { audioTrack.flush(); audioTrack.release(); audioTrack = null; } // clear source and the other globals sourcePath = null; sourceRawResId = -1; duration = 0; mime = null; sampleRate = 0; channels = 0; bitrate = 0; presentationTimeUs = 0; duration = 0; state.set(PlayerStates.STOPPED); stop = true; if(noOutputCounter >= noOutputCounterLimit) { if (events != null) handler.post(new Runnable() { @Override public void run() { events.onError(); } }); } else { if (events != null) handler.post(new Runnable() { @Override public void run() { events.onStop(); } }); } } public static String listCodecs() { String results = ""; int numCodecs = MediaCodecList.getCodecCount(); for (int i = 0; i < numCodecs; i++) { MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); // grab results and put them in a list String name = codecInfo.getName(); boolean isEncoder = codecInfo.isEncoder(); String[] types = codecInfo.getSupportedTypes(); String typeList = ""; for (String s:types) typeList += s + " "; results += (i+1) + ". " + name+ " " + typeList + "\n\n"; } return results; } }