I am working on an Android application which does lots of concurrencies and RxJava treading in the background and it is possible to not want to play a music which is in the async preparation phase for some reason:
player.prepareAsync()
and
override fun onPrepared(mp: MediaPlayer?) {
isPreparing = false
if (playAfterPreparing) {
//start playback
mp?.start()
} else {
// don't play and keep it prepare for later!!!
}
updateNotification()
}
but the problem is after preparation you should play(!) because:
Media Player called in state 0, error (-38,0)
I even tried to start and pause one after another in preparation:
override fun onPrepared(mp: MediaPlayer?) {
Timber.i("onPrepared")
Timber.i("playAfterPreparing: $playAfterPreparing")
isPreparing = false
//start playback
mp?.start()
if (!playAfterPreparing) {
mp?.pause()
}
updateNotification()
}
but it didn't work cause it rendered to the same issue.
I also know how MediaPlayer state machine is working, but don't see anything in this situation in documents:
https://developer.android.com/reference/android/media/MediaPlayer.html
any solution for such a situation?
Thanks to #VladyslavMatviienko comments, I suspected that I am doing something wrong somewhere else and he was correct. According to Android MediaPlayer document:
It is possible to stay in Prepared state, but it is not possible to call any method namely pause() and that was my fault. In Prepared state You can only call start(), seekTo() or stop().
Hope this answer comes handy for those who might face this issue later.
Related
I've been working on a navigation feature for a maps app which has voice instructions. The problem is that when announcing the instructions, the first 500 milliseconds of the instruction gets cut off. For eg, if the instruction is "In 200m turn right", in the bluetooth earphone it ends up sounding like "200m turn right". Or if the instruction is "Continue for 2 kilometers", then it sounds like "tinue for 2 kilometers".
This is the code I'm using for TTS -
//Initialisation happens only once
var textToSpeechEngine = TextToSpeech(this) { status ->
if (status == TextToSpeech.SUCCESS) {
textToSpeechEngine?.language = Locale.ENGLISH
textToSpeechEngine?.setSpeechRate(0.8f)
}
}
...
//When text to speak is ready, invoking the speak method
textToSpeechEngine?.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, null, "tts1")
Additionally, I'm also using AudioFocusRequest to request and abandon focus so that any music playing in the background lowers its volume while the instruction is being spoken. This is the code I'm using for that -
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
var focusRequest: AudioFocusRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build())
.setAcceptsDelayedFocusGain(false)
.build()
audioManager.requestAudioFocus(focusRequest)
} else {
audioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
}
textToSpeechEngine?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
fun abandonFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusRequest?.let { request -> audioManager.abandonAudioFocusRequest(request) }
} else {
audioManager.abandonAudioFocus(null)
}
}
override fun onStart(utteranceId: String?) {}
override fun onDone(utteranceId: String?) {
abandonFocus()
}
override fun onError(utteranceId: String?) {
abandonFocus()
}
})
Point to note here is that this issue is not happening in all the bluetooth devices I've tested with. The issue happened with 2 Chinese brands bluetooth earphones but is not happening with a bluetooth Sony over-the-head headset. Also, the issue does not happen if music is playing in the background while navigation is going on.
As I understand it, it seems that the cheaper bluetooth earphones seem to keep the "connection alive" only when audio is actively coming through else it stops the connection temporarily in order to save battery I guess? However, when music is playing in the background, the connection is kept alive constantly so the instruction speech does not get cut off.
What can I do to fix this or work around it?
note that requestAudioFocus may take a OnAudioFocusChangeListener as first param, you are passing null in there (also Builder have this param). switching focus may take some (short) time, so I would recomend to fire your TTS when you get this callback fired with AUDIOFOCUS_GAIN
Assuming that you're correct that it is purely hardware at fault (which is what it seems like):
You can use the playSilence() or playSilentUtterance() methods of the TextToSpeech class to play silence for 500ms prior to your main speak() command... which should fool the speakers/headphones.
It may help to also use QUEUE_ADD instead of QUEUE_FLUSH for your main speak() request to make sure that it it attached the the previous (silence) with no gap... and that it doesn't prematurely end the first (silent) utterance.
There are a lot of variables at play here. You could consider:
Can you find an app that uses TextToSpeech which DOES work correctly even on these problematic devices? If so, then the problem must be solvable in code, and maybe you could find the source code for the app you tested and look at how they are setting up the AudioManager.
I am using cordova 6.4.0 with cordova-plugin-media for streaming radio-stations in an Android Application. Unfortunately there is a case, where the application is not responding properly anymore.
Let's say the user wants to stream a radiostation, but while the stream is loading, he wants to abort it (for example because the stream is down, or taking very long to load).
In this case I am not able to cancel the process!
media = new Media("http://direct.franceinfo.fr/live/franceinfo-midfi.mp3?ID=f9fbk29m84", mediaPlayerSuccess, mediaPlayerFail, mediaPlayerStatus);
media.play();
Now I want to cancel the process of buffering the stream, but I'm not able to. The functions:
media.pause();
media.stop();
are throwing error messages in the ADB-log and are calling the mediaPlayer-onError callback.
D/AudioPlayer( 3362): AudioPlayer Error: pausePlaying() called during invalid state: 1
...
D/AudioPlayer( 3362): AudioPlayer Error: stopPlaying() called during invalid state: 1
The media.release() command stops the loading of the stream! However just releasing the stream without stopping it, causes other, rather big problems:
Most of the times the system reacts just very slow and hangs a few seconds, if you call media.release() on a media-object. But if you do this often, the system completly freezes. Meaning it does not accetp remote-control commands anymore.
The Adb-log is still working, but does not show any errors in this case. Only the POWER-Button is still working (it locks and unlocks the screen). The only way to recover from this screwed-up state, is to reboot the device.
How am I supposed to cancel a Media-stream if it is not playing, yet? Is this a bug in the plugin?
Attached is the code-snippet, that I use to handle the media-streaming-logic. Like described above... it basically works, but it slows down or even freezes device, if you call it multiple times.
function radioControl(action, media_src){
//media_src is a webradio-streamurl.
if(action == 'play') {
// Initial Play
if(media === null){
mediaCreateObject(media_src);
}
// If we get PLAY but on antoher station
else if(media.src != media_src){
mediaReleaseRessources();
mediaCreateObject(media_src);
}
//interrupt_timer = false;
if(media === null){
mediaCreateObject(media_src);
}
media.play();
}
else if (action === 'pause') {
//If we get "pause", but it didn't even start yet
if(media._duration == -1){
mediaReleaseRessources();
}
else{
media.pause();
}
}
}
function mediaCreateObject(media_src){
media = new Media(media_src, mediaPlayerSuccess, mediaPlayerFail, mediaPlayerStatus);
}
function mediaReleaseRessources(){
media.release();
}
I found out, that this is not a cordova issue, but an 8 year-old (!) android-bug, that was never fixed. See here:
https://code.google.com/p/android/issues/detail?id=959
MediaPlayer "crash" (deadlocks the calling thread) when resetting or releasing an unused MediaPlayer
Basically the problem is: If you try to "release" a media-object that is not playing (yet), it will deadlock the calling thread, which causes the freezing that I have mentioned in the question. Unfortunately they never fixed this bug, but just marked it as "obsolete". In Android 5.1.1. the bug apparently is still there. Maybe they fixed it in later versions.
I have made a rather ugly workaround for this problem, but it is working. Basically what I did is:
We save every media-object in a javaScript-object. If the user stops it, while it plays, we can just stop and delete the object. But if it is not playing, we leave this media-object in this javaScript-object media_objects = {};
Also we save the currently active_media stream in a variable.
If cordova calls the mediaPlayerStatusChange-callback we loop through the media_objects and check if the status of one of the "pending"-objects has now changed to "running". - Cordova justs calls the media-status-change-callback without any indictation what media-object exactly just changed the state. That is unfortunate, so we have to check if one of the pending-"obsolete" objects now started playing. If so, we can stop and release it. (If the object is actually playing, stop and release works like intended - only if it's not playing, it causes the crash)
function mediaPlayerStatusChange(status){
mediaReleaseRessources();
// handle status change....
// ......
}
function mediaReleaseRessources(){
for(var key in media_objects) {
// We can only stop-and release an object, if it is playing
// If an object started playing, the "_duration"-value is != -1
if(key !== active_media && media_objects[key]._duration != -1) {
media_objects[key].stop();
media_objects[key].release();
delete media_objects[key];
}
}
}
This solution works for me, however I am still interested in a better and cleaner way to handle multiple media-streams in cordova.
I have an important question: I manage stream flow on my receiver but I want to use stop message from my sender for stop the stream. Currently I'm using this sender (https://github.com/googlecast/CastVideos-android) and this receiver (https://github.com/googlecast/CastReferencePlayer) but the problem is that the sender sends me a command to pause and not a stop during the live.
In the documentation, side sender Android, I'm reading that the behavior is correct and the receiver must send a media status update message back to the sender and should report the state as MediaStatus.PLAYER_STATE_IDLE with reason MediaStatus.IDLE_REASON_CANCELLED.
Arrived at this point I want to modify the sample receiver and manage the pause like a stop and not like a pause. I want to manage the stop because when the sender click the stop button the receiver must stop the video and the sender must destroy "every player" (player, miniplayer, etc).
So My idea is this:
sampleplayer.CastPlayer.prototype.onPause_ = function() {
this.log_('onPause');
this.cancelDeferredPlay_('media is paused');
var isIdle = this.state_ === sampleplayer.State.IDLE;
var isDone = this.mediaElement_.currentTime === this.mediaElement_.duration;
var isUnderflow = this.player_ && this.player_.getState()['underflow'];
if (isUnderflow) {
this.log_('isUnderflow');
this.setState_(sampleplayer.State.BUFFERING, false);
this.mediaManager_.broadcastStatus(/* includeMedia */ false);
} else if (!isIdle && !isDone) {
this.setState_(sampleplayer.State.PAUSED, false);
} else if(this.isLiveStream) {
this.log_('onStop');
this.cancelDeferredPlay_('media is stopped');
var self = this;
sampleplayer.transition_(self.element_, sampleplayer.TRANSITION_DURATION_,
function() {
self.setState_(sampleplayer.State.IDLE, false);
self.resetMediaElement_();
self.mediaManager_.setIdleReason("CANCELLED");
});
return ;
}
this.updateProgress_();
};
As you can see in the third branc, I control a variable that I saved during the load of the video and after I set the state of player to IDLE, reset the mediaElement and finally I send to broadcast the State = IDLE and Reason = CANCELLED. In this way the sender see the message because I look the log but doesn't interpret this.
Now I don't know how to continue and manage this behaviour. Advice are welcome. Thank you.
Just to make sure my answer is covering your question, let me simplify your questions as the following: when playing a live stream, you want to still see the "stop" button on the sender side but if the user taps on that button, you want to stop the playback and unload the media.
If this is correct, you can achieve that from the sender side as well: CastVideos-android uses CastCompanionLibrary. There are two places that you need to update in that library: in VideoCastManager#togglePlayback() and VideoCastControllerFragment#togglePlayback(). In the former place, you need to update the conditional there to read like the following:
if (isPlaying && isRemoteStreamLive()) {
stop();
} else if (isPlaying) {
pause();
} else {
... //leave as is
}
In the latter case, you need to update one of the switch statements:
case MediaStatus.PLAYER_STATE_PLAYING:
if (mSelectedMedia.getStreamType() == MediaInfo.STREAM_TYPE_LIVE) {
mCastManager.stop();
mPlaybackState = MediaStatus.PLAYER_STATE_IDLE;
} else {
mCastManager.pause();
mPlaybackState = MediaStatus.PLAYER_STATE_BUFFERING;
}
break;
Basically, you are adding a logic that if the content is playing remotely, "toggling playback" should call stop() rather than pause() when dealing with a live stream.
Note that calling stop() means you are completely unloading your media and you may need to do some additional work to play another media but I haven't tested that since I don't call stop() in the CastVideos app. Also note that I have not tested/tried the above suggestion since I don't have a live stream to play with but I am hoping it would work.
I work with a MediaPlayer and set the state of the player often programmatically like for example:
if(mp.isPlaying()) {
mp.pause();
animationPausedMusic();
}
private void animationPausedMusic() {
// Changing button image to play button
btn_play.setBackgroundResource(R.drawable.play);
... // more code
}
But sometimes the logcat gives me the message:
"internal/external state mismatch corrected"
And then the play and pause function is not working anymore.
What does this message mean? And how can I solve it?
After going through the android's native framework for media player I found that in source file mediaplayer.cpp inside function bool MediaPlayer::isPlaying() The developer is checking if the currentState of media player is in STARTED state and yet the media player is not playing any media, so it tries to change the state to PAUSED state so that the state consistency should be maintained for API users.(and here is where he is printing the message "ALOGE("internal/external state mismatch corrected");")
Now If you go through the media player state diagram below:
You would notice that this may happen when the MediaPlayer moved to 'STARTED' state after a call to start() and at this time for some obscure reason has not yet started the playback and you fire a MediaPlayer.isPlaying() method call , The Framework treat this as state inconsistency and moves to 'PAUSED' state and that's why you cannot see anything playing further.
However, if someone has some better understanding please share your thoughts!
I ran into this recently, and like some other questions say, it's this bug (marked obsolete alas)
https://code.google.com/p/android/issues/detail?id=9732
I found this error occurs when playing a MIDI file, but only sometimes. It happens when mp.isPlaying() is called quickly after mp.start()
If you can manage to not call mp.isPlaying() for a little bit, the error doesn't occur. In my case, a 10th of a second or so made the difference between getting the error or not. It's awkward, but it works.
e.g.
//setting a new track
mp.setDataSource(path);
mp.prepare();
mp.start();
//calling mp.isPlaying() here or shortly after starts the problem
//since we know it's playing, we can store that state, or call
updateUiPlaying(); //eg instead of updateUi();
//or just call some code here that takes more time first
updateScaledImages(); //something that might take time
Log.v(TAG, "mp.isPlaying = " + mp.isPlaying()); //now isPlaying() shouldn't cause that error
Also, I put a check in when I pause later.
mp.pause()
if(mp.isPlaying()){
//shouldn't be playing, must be in error
mp.stop();
mp.release();
mp = new MediaPlayer();
//any other initialization here
}
Though the problem doesn't occur if there is a wait before calling isPlaying()
Apparently there is more than one cause of this message. The following solution worked for me. It may or may not work for you. I called the method MediaPlayer.reset() immediately after instantiating the MediaPlayer object:
MediaPlayer mp = new MediaPlayer();
mp.reset();
I am using a MediaPlayer. Somewhere inside my code I use:
if (mp != null) {
System.out.println("This");
if (mp.isPlaying()) //1
System.out.println("That"); //2
}
And as I run it, the app for some reason stops from working. If I delete the //1 and //2 lines, is runs normally and it prints "This". But why, I just want to check if mp is playing or not
as you can see on Android MediaPlayer documentations
public boolean isPlaying ()
Added in API level 1 Checks whether the MediaPlayer is playing.
Returns true if currently playing, false otherwise Throws
IllegalStateException if the internal player engine has not been
initialized or has been released.
check if you are initialising the MediaPlayer correctly and that you haven't released it before calling isPlaying().
please mark it as correct answer if that helped you.