I'm working on an app that streams a list of mp3 files, to do this I've used ExoPlayer with a ConcatenatingMediaSource as this:
private fun createMediaSource(
tracks: List<Track>
): MediaSource = ConcatenatingMediaSource(true).apply {
tracks.forEach { track ->
val mediaSource = ProgressiveMediaSource
.Factory(DefaultDataSourceFactory(context))
.createMediaSource(MediaItem.fromUri(track.getFullUri()))
addMediaSource(mediaSource)
}
}
This works great, the files play as list with no errors at all, however what's required from me is to play all these streams as a single stream, where I show the total length of all streams on the seek bar, and the user would seek seamlessly between them.
Of course I'm not using the VideoPlayer provided by ExoPlayer because I need the seekbar to span all media sources, which apparently this is not possible to do with ExoPlayerUi.
So this is the logic I've used when the user tries to seek:
exoPlayer.apply {
var previousTracksLength = 0L
var windowIndex = 0
var currentItemLength = 0L
run loop#{
tracksList.forEachIndexed { index, track ->
currentItemLength = track.getLengthMillis()
previousTracksLength += currentItemLength
if (newPositionMillis < previousTracksLength) {
windowIndex = index
return#loop
}
}
}
val positionForCurrentTrack = (newPositionMillis - (previousTracksLength - currentItemLength))
pause()
if (windowIndex == currentWindowIndex) {
seekTo(positionForCurrentTrack)
} else {
seekTo(windowIndex, positionForCurrentTrack)
}
play()
}
This works amazingly well when the ConcatenatingMediaSource has only 3 or less media sources, but if it's bigger than that, weird behavior starts showing up, I might just want to seek 10 seconds forward the player would move more than 2 minutes instead.
After debugging it was obvious for me that when I call: seekTo(windowIndex, positionForCurrentTrack) exoPlayer is seeking to a window that's not mapped with a specific media source in the ConcatenatingMediaSource !
And here comes my questions:
Does ExoPlayer create a single window for each mediaSource in the ConcatenatingMediaSource or not ?
and If not is there a way to force it to do that ?
This is not really an answer but the explanation to why when I called seekTo(windowIndex, position) the player seemed like it was ignoring the windowIndex and actually seek to a completely unexpected position is because the media type was mp3 !
Apparently many devs have suffered the same issue where the player seek position is out of sync with the real position of the media that's being played when it's an mp3.
More details for anyone having weird issues when playing mp3 using ExoPlayer
https://github.com/google/ExoPlayer/issues/6787#issuecomment-568180969
Related
I have two instances of androidx.media2.player.MediaPlayer, one for playing .mp4, one for playing .wav. I want them both to play audio simultaneously.
I am using setAudioAttributes for two players, but as soon as I set the attributes for both player, none of them plays sound, and the video player doesn't even play video.
val soundFile = course.header.directory + soundFileName
val file = File(soundFile)
val fileDescriptor = ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_ONLY)
val mediaItem = FileMediaItem.Builder(fileDescriptor).build()
audioPlayer.setMediaItem(mediaItem)
audioPlayer.setAudioAttributes(AudioAttributesCompat.Builder()
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.build()
)
audioPlayer.prepare().addListener({
videoPlayer.play()
audioPlayer.play()
}, ContextCompat.getMainExecutor(context))
I set attributes to videoPlayer in a similar way.
I've been trying to set the same session id to both players, but it didn't help.
Blocking the thread for five seconds appears to work around the problem, but I don't know why:
audioPlayer.prepare().addListener({
videoPlayer.play()
audioPlayer.play()
Thread.sleep(5000)
}, ContextCompat.getMainExecutor(context))
I haven't found a solution. I've moved on to ExoPlayer, which doesn't have setAudioAttributes.
I'm working an an App that plays Video files. I'm using android MediaPlayer class to play Video files.
Problem:
I want to play let say a video file with multiple embedded Audio Tracks. And then want to allow users choose between the tracks at runtime through an interface.
Is it even possible with Android MediaPlayer?
I've seen many application that has this feature like MX PLayer, VLC for android ...
Yes Android MediaPlayer supports playback for multiple embedded Audio Tracks.
You can use selectTrack API to achieve the same.
Syntax goes as below.
public void selectTrack (int index)
index int: 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 getTrackInfo() method.
Example usage:
MediaPlayer mplayer = new MediaPlayer();
MediaPlayer.TrackInfo[] trackInfo = mplayer.getTrackInfo();
for (int i = 0; i < trackInfoArray.length; i++) {
if (trackInfo[i].getTrackType() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
mplayer.selectTrack(i);
break;
}
I want to implement a beat matching Crossfade feature using ExoPlayer. Basically I have a concept how it should work, but I find it hard to adapt it to ExoPlayer.
Let me please first write how I want to do this so you can understand the case.
As you probably know Beat Matching Crossfade let to seamlessly switch from one song to another. Additionally it adjusts second song tempo to the first song tempo during the crossfade.
So my plan is as follows:
1. Load song A and B so they both starts to buffer.
2. Decoded samples of song A and B are stored in buffers BF1 and BF2.
3. There would be a class called MUX which is a main buffer and contains both songs buffers, BF1 and BF2. MUX provides audio samples to the Player. Samples provided to the Player are BF1 samples or mixed samples from BF1 and BF2 if there is a crossfade.
4. When buffer reaches the crossfade point then samples are send to Analyser class so it can analyse samples from both buffers and modify them for crossfade. Analyser sends modified samples to MUX which updates it's main buffer.
When crossfade is finished then load a next song from playlist.
My main question is how to mix two songs so I can implement class like MUX.
What I know so far is that I can access decoded samples in MediaCodecRender.processOutputBuffer() method so from that point I could create my BF1 and BF2 buffers.
There was also an idea to create two instances of ExoPlayer and while first song is playing the second one is analysed and it's samples are modified for further crossfade, but I think it may be hard to synchronise two players so the beats would match.
Thanks in advance for any help!
Answering #David question about crossfade implementation it looks more or less like this. You have to listen for active player playback and call this method when you want to start crossfade. It's in RxJava but can be easily migrated to Couroutines Flow.
fun crossfadeObservable(
fadeOutPlayer: Player?,
fadeInPlayer: Player,
crossfadeDurationMs: Long,
crossfadeScheduler: Scheduler,
uiScheduler: Scheduler
): Observable<Unit> {
val fadeInMaxGain = fadeInPlayer.audioTrack?.volume ?: 1f
val fadeOutMaxGain = fadeOutPlayer?.audioTrack?.volume ?: 1f
fadeOutPlayer?.enableAudioFocus(false)
fadeInPlayer.enableAudioFocus(true)
fadeInPlayer.playerVolume = 0f
fadeInPlayer.play()
fadeOutPlayer?.playerVolume = fadeOutMaxGain
fadeOutPlayer?.play()
val iterations: Float = crossfadeDurationMs / CROSSFADE_STEP_MS.toFloat()
return Observable.interval(CROSSFADE_STEP_MS, TimeUnit.MILLISECONDS, crossfadeScheduler)
.take(iterations.toInt())
.map { iteration -> (iteration + 1) / iterations }
.filter { percentOfCrossfade -> percentOfCrossfade <= 1f }
.observeOn(uiScheduler)
.map { percentOfCrossfade ->
fadeInPlayer.playerVolume = percentOfCrossfade.coerceIn(0f, fadeInMaxGain)
fadeOutPlayer?.playerVolume = (fadeOutMaxGain - percentOfCrossfade).coerceIn(0f, fadeOutMaxGain)
}
.last()
.doOnTerminate { fadeOutPlayer?.pause() }
.doOnUnsubscribe { fadeOutPlayer?.pause() }
}
const val CROSSFADE_STEP_MS = 100L
I'm using ExoPlayer 2.3.1 for playing the list of videos. I'm using code from sample:
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources);
player.prepare(mediaSource, !haveResumePosition, false);
It's working fine, but I want to select video to play first.
For example I have 3 videos, when I clicks on second it should play and prev/next buttons in player should be available. Now it starts playing from the first mediaSource in array, and I can't find a way to set index of initial track or something like that.
Please check this out seekTo(windowIndex, positionMs). It says
Seeks to a position specified in milliseconds in the specified window.
Example:
player.seekTo(3, C.TIME_UNSET);
player.setPlayWhenReady(true);
I am working on an Android project that involves the use of Google's ExoPlayer.
I have a list of video sources which I build a playlist from using the following code:
for (int i = 0; i < vidList.length(); i++) {
MediaSource source = new ExtractorMediaSource(Uri.parse(vidList.getJSONObject(i).getString("url")),
buildDataSourceFactory(bandwidthMeter), extractorsFactory, mainHandler, HomeFragment.this);
mediaSources.add(source);
captions.add(vidList.getJSONObject(i).getString("caption"));
}
mediaSource = new ConcatenatingMediaSource(mediaSources.toArray(new MediaSource[mediaSources.size()]));
I then call
exoplayer.prepare(mediasource, false, false)
and the videos play in succession fine. I would like to display the caption of the currently playing video in a textView and so I have a separate list that holds the "caption" values for each video.
From scouring through the code I see that I can get the currently playing video in the playlist like this;
exoPlayer.getCurrentPeriodIndex()
Which seems to work and returns the index except for one problem. It returns the value of 0 twice as playback starts. That is video at index 0 returns period 0 as well as video at index 1. This only occurs at indexes 0 and 1 and thereafter everything else looks fine except that the getCurrentPeriodIndex() will return theAccurateIndex - 1.
I see this also happening in the demo Exoplayer application.
Is there a better way to determine what track is currently playing in the playlist?
Thanks.
To find the currently playing track, you need to reference currentWindowIndex exoPlayer field. Looks like this in Java...
exoPlayer.getCurrentWindowIndex()
I'm not sure what getCurrentPeriodIndex() does, and the docs don't elaborate, and I don't like speculating.
exoPlayer.getCurrentWindowIndex() is Deprecated.
Use exoPlayer.getCurrentMediaItemIndex() instead.