/* * Copyright (C) 2009 Teleca Poland Sp. z o.o. <android@teleca.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 com.teleca.jamendo.api_impl; import java.io.IOException; import com.teleca.jamendo.MyApplication; import com.teleca.jamendo.activity.playview.PlayMethod; import com.teleca.jamendo.api.IPlayEngine; import com.teleca.jamendo.api.IPlayEngineListener; import com.teleca.jamendo.model.Playlist; import android.media.MediaPlayer; import android.media.MediaPlayer.OnBufferingUpdateListener; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnPreparedListener; import android.os.Handler; import android.util.Log; /** * Player core engine allowing playback, in other words, a * wrapper around Android's <code>MediaPlayer</code>, supporting * <code>Playlist</code> classes * * @author Lukasz Wisniewski */ public class PlayerEngineImpl implements IPlayEngine { /** * Time frame - used for counting number of fails within that time */ private static final long FAIL_TIME_FRAME = 1000; /** * Acceptable number of fails within FAIL_TIME_FRAME */ private static final int ACCEPTABLE_FAIL_NUMBER = 2; /** * Beginning of last FAIL_TIME_FRAME */ private long mLastFailTime; /** * Number of times failed within FAIL_TIME_FRAME */ private long mTimesFailed; /** * Simple MediaPlayer extensions, adds reference to the current track * * @author Lukasz Wisniewski */ private class InternalMediaPlayer extends MediaPlayer { /** * Keeps record of currently played track, useful when dealing * with multiple instances of MediaPlayer */ public Playlist playlistEntry; /** * Still buffering */ public boolean preparing = false; /** * Determines if we should play after preparation, * e.g. we should not start playing if we are pre-buffering * the next track and the old one is still playing */ public boolean playAfterPrepare = false; } /** * InternalMediaPlayer instance (maybe add another one for cross-fading) */ private InternalMediaPlayer mCurrentMediaPlayer; /** * Listener to the engine events */ private IPlayEngineListener mPlayerEngineListener; /** * Playlist */ private PlayMethod mPlaylist = null; /** * Handler to the context thread */ private Handler mHandler; /** * Runnable periodically querying Media Player * about the current position of the track and * notifying the listener */ private Runnable mUpdateTimeTask = new Runnable() { public void run() { if(mPlayerEngineListener != null){ // TODO use getCurrentPosition less frequently (usage of currentTimeMillis or uptimeMillis) if(mCurrentMediaPlayer != null) mPlayerEngineListener.onTrackProgress(mCurrentMediaPlayer.getCurrentPosition()/1000); mHandler.postDelayed(this, 1000); } } }; /** * Default constructor */ public PlayerEngineImpl() { mLastFailTime = 0; mTimesFailed = 0; mHandler = new Handler(); } @Override public void next() { mPlaylist.selectNext(); play(); } @Override public void openPlaylist(PlayMethod playlist) { if(!playlist.isEmpty()) mPlaylist = playlist; else mPlaylist = null; } @Override public void pause() { if(mCurrentMediaPlayer != null){ // still preparing if(mCurrentMediaPlayer.preparing){ mCurrentMediaPlayer.playAfterPrepare = false; return; } // check if we play, then pause if(mCurrentMediaPlayer.isPlaying()){ mCurrentMediaPlayer.pause(); if(mPlayerEngineListener != null) mPlayerEngineListener.onTrackPause(); return; } } } @Override public void play() { if( mPlayerEngineListener.onTrackStart() == false ){ return; // apparently sth prevents us from playing tracks } // check if there is anything to play if(mPlaylist != null){ // check if media player is initialized if(mCurrentMediaPlayer == null){ mCurrentMediaPlayer = build(mPlaylist.getSelectedTrack()); } // check if current media player is set to our song if(mCurrentMediaPlayer != null && mCurrentMediaPlayer.playlistEntry != mPlaylist.getSelectedTrack()){ cleanUp(); // this will do the cleanup job mCurrentMediaPlayer = build(mPlaylist.getSelectedTrack()); } // check if there is any player instance, if not, abort further execution if(mCurrentMediaPlayer == null) return; // check if current media player is not still buffering if(!mCurrentMediaPlayer.preparing){ // prevent double-press if(!mCurrentMediaPlayer.isPlaying()){ // i guess this mean we can play the song Log.i(MyApplication.TAG, "Player [playing] "+mCurrentMediaPlayer.playlistEntry.getTrack().getName()); // starting timer mHandler.removeCallbacks(mUpdateTimeTask); mHandler.postDelayed(mUpdateTimeTask, 1000); mCurrentMediaPlayer.start(); } } else { // tell the mediaplayer to play the song as soon as it ends preparing mCurrentMediaPlayer.playAfterPrepare = true; } } } @Override public void prev() { mPlaylist.selectPrev(); play(); } @Override public void skipTo(int index) { mPlaylist.select(index); play(); } @Override public void stop() { cleanUp(); if(mPlayerEngineListener != null){ mPlayerEngineListener.onTrackStop(); } } /** * Stops & destroys media player */ private void cleanUp(){ // nice clean-up job if(mCurrentMediaPlayer != null) try{ mCurrentMediaPlayer.stop(); } catch (IllegalStateException e){ // this may happen sometimes } finally { mCurrentMediaPlayer.release(); mCurrentMediaPlayer = null; } } private InternalMediaPlayer build(Playlist playlistEntry){ final InternalMediaPlayer mediaPlayer = new InternalMediaPlayer(); // try to setup local path String path = MyApplication.getInstance().getDownloadInterface().getTrackPath(playlistEntry); if(path == null) // fallback to remote one path = playlistEntry.getTrack().getStream(); // some albums happen to contain empty stream url, notify of error, abort playback if(path.length() == 0){ if(mPlayerEngineListener != null){ mPlayerEngineListener.onTrackStreamError(); mPlayerEngineListener.onTrackChanged(mPlaylist.getSelectedTrack()); } stop(); return null; } try { mediaPlayer.setDataSource(path); mediaPlayer.playlistEntry = playlistEntry; //mediaPlayer.setScreenOnWhilePlaying(true); mediaPlayer.setOnCompletionListener(new OnCompletionListener(){ @Override public void onCompletion(MediaPlayer mp) { next(); } }); mediaPlayer.setOnPreparedListener(new OnPreparedListener(){ @Override public void onPrepared(MediaPlayer mp) { mediaPlayer.preparing = false; // we may start playing if(mPlaylist.getSelectedTrack() == mediaPlayer.playlistEntry && mediaPlayer.playAfterPrepare){ mediaPlayer.playAfterPrepare = false; play(); } } }); mediaPlayer.setOnBufferingUpdateListener(new OnBufferingUpdateListener(){ @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { if(mPlayerEngineListener != null){ mPlayerEngineListener.onTrackBuffering(percent); } } }); mediaPlayer.setOnErrorListener(new OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { Log.w(MyApplication.TAG, "PlayerEngineImpl fail, what ("+what+") extra ("+extra+")"); if(what == MediaPlayer.MEDIA_ERROR_UNKNOWN){ // we probably lack network if(mPlayerEngineListener != null){ mPlayerEngineListener.onTrackStreamError(); } stop(); return true; } // not sure what error code -1 exactly stands for but it causes player to start to jump songs // if there are more than 5 jumps without playback during 1 second then we abort // further playback if(what == -1){ long failTime = System.currentTimeMillis(); if(failTime - mLastFailTime > FAIL_TIME_FRAME){ // outside time frame mTimesFailed = 1; mLastFailTime = failTime; Log.w(MyApplication.TAG, "PlayerEngineImpl "+mTimesFailed+" fail within FAIL_TIME_FRAME"); } else { // inside time frame mTimesFailed++; if(mTimesFailed > ACCEPTABLE_FAIL_NUMBER){ Log.w(MyApplication.TAG, "PlayerEngineImpl too many fails, aborting playback"); if(mPlayerEngineListener != null){ mPlayerEngineListener.onTrackStreamError(); } stop(); return true; } } } return false; } }); // start preparing Log.i(MyApplication.TAG, "Player [buffering] "+mediaPlayer.playlistEntry.getTrack().getName()); mediaPlayer.preparing = true; mediaPlayer.prepareAsync(); // this is a new track, so notify the listener if(mPlayerEngineListener != null){ mPlayerEngineListener.onTrackChanged(mPlaylist.getSelectedTrack()); } return mediaPlayer; } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } @Override public PlayMethod getPlaylist() { return mPlaylist; } @Override public boolean isPlaying() { // no media player instance if(mCurrentMediaPlayer == null) return false; // so there is one, let's see if it's not preparing if(mCurrentMediaPlayer.preparing) return false; // finally return mCurrentMediaPlayer.isPlaying(); } @Override public void setListener(IPlayEngineListener playerEngineListener) { mPlayerEngineListener = playerEngineListener; } }