Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic playlists with Exoplayer 2

I'd like to use ExoPlayer2 with playlists having possibility to dinamically change the tracks (add or remove them from playlist) and change the loop settings.

Since ConcatenatingMediaSource has static arrays (and not lists), I'm implementing a DynamicMediaSource, like Concatenating one but with lists instead of arrays and one mode method addSource to add one more media source to the list.

public void addSource(MediaSource mediaSource) {
    this.mediaSources.add(mediaSource);
    duplicateFlags = buildDuplicateFlags(this.mediaSources);
    if(!mediaSources.isEmpty())
        prepareSource(mediaSources.size() -1);
    else
        prepareSource(0);
}

When I invoke addSource

                MediaSource ms = buildMediaSource(mynewuri, null);
                mediaSource.addSource(ms);

the track is added to the arrays but it seems something is missing because I always obtain ArrayOutOfBoundsException in createPeriod method.

In createPeriod the method

mediaSources.get(sourceIndex)...

is trying to access the index = mediaSources.size().

Can you help me?

like image 550
Manuela Avatar asked Sep 21 '16 09:09

Manuela


People also ask

What is ExoPlayer v2?

ExoPlayer is an application level media player for Android. It provides an alternative to Android's MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android's MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks.

How do I play the next ExoPlayer video on Android?

Show activity on this post. 3] Just check by clicking next button from the media controller if that works then you are done, now the videos will be played automatically once finished the current one.

Does ExoPlayer support m3u8?

mpd or . m3u8 video file from server using FFMPEG. Exoplayer does not support mp4 file format for adaptive streaming but you can choose mp4 like format for auto streaming. Auto streaming is something which is completely depends on speed and connectivity of the internet.

What is track selection in ExoPlayer?

Track selection is the responsibility of a TrackSelector , an instance of which can be provided whenever an ExoPlayer is built and later obtained with ExoPlayer. getTrackSelector() . DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); ExoPlayer player = new ExoPlayer. Builder(context) .


1 Answers

I eventually managed it. It was my fault during the conversion from arrays to lists. I had to use SparseArrays for timelines and manifests and everything began to work.

In the DynamicMediaSource simply set the following types:

private final List<MediaSource> mediaSources;
private final SparseArray<Timeline> timelines;
private final SparseArray<Object> manifests;
private final Map<MediaPeriod, Integer> sourceIndexByMediaPeriod;
private SparseArray<Boolean> duplicateFlags;

you have to use sparse arrays to set the proper values into the timelines and manifests in the method

private void handleSourceInfoRefreshed(int sourceFirstIndex, Timeline sourceTimeline,
                                       Object sourceManifest) {
    // Set the timeline and manifest.
    timelines.put(sourceFirstIndex, sourceTimeline);
    manifests.put(sourceFirstIndex, sourceManifest);

    // Also set the timeline and manifest for any duplicate entries of the same source.
    for (int i = sourceFirstIndex + 1; i < mediaSources.size(); i++) {
        if (mediaSources.get(i).equals(mediaSources.get(sourceFirstIndex))) {
            timelines.put(i, sourceTimeline);
            manifests.put(i, sourceManifest);
        }
    }

    for(int i= 0; i<mediaSources.size(); i++){
        if(timelines.get(i) == null){
            // Don't invoke the listener until all sources have timelines.
            return;
        }
    }

    timeline = new DynamicTimeline(new ArrayList(asList(timelines)));
    listener.onSourceInfoRefreshed(timeline, new ArrayList(asList(manifests)));
}

Here is the complete code of DynamicMediaSource class:

public final class DynamicMediaSource implements MediaSource {

private static final String TAG = "DynamicSource";

private final List<MediaSource> mediaSources;
private final List<Timeline> timelines;
private final List<Object> manifests;
private final Map<MediaPeriod, Integer> sourceIndexByMediaPeriod;
private SparseArray<Boolean> duplicateFlags;

private Listener listener;
private DynamicTimeline timeline;

/**
 * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
 *                     {@link MediaSource} instance to be present more than once in the array.
 */
public DynamicMediaSource(MediaSource... mediaSources) {
    this.mediaSources = new ArrayList<MediaSource>(Arrays.asList(mediaSources));
    timelines = new ArrayList<Timeline>();
    manifests = new ArrayList<Object>();
    sourceIndexByMediaPeriod = new HashMap<>();
    duplicateFlags = buildDuplicateFlags(this.mediaSources);
}

public void addSource(MediaSource mediaSource) {
    this.mediaSources.add(mediaSource);
    duplicateFlags = buildDuplicateFlags(this.mediaSources);
    /*if(!mediaSources.isEmpty())
        prepareSource(mediaSources.size() -1);
    else
        prepareSource(0);*/
}

@Override
public void prepareSource(Listener listener) {
    this.listener = listener;
    for (int i = 0; i < mediaSources.size(); i++) {
        prepareSource(i);
        /*if (duplicateFlags.get(i) == null || !duplicateFlags.get(i)) {
            final int index = i;
            mediaSources.get(i).prepareSource(new Listener() {
                @Override
                public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
                    handleSourceInfoRefreshed(index, timeline, manifest);
                }
            });
        }*/
    }
}

private void prepareSource(int sourceindex) {
    if (duplicateFlags.get(sourceindex) == null || !duplicateFlags.get(sourceindex)) {
        final int index = sourceindex;
        mediaSources.get(sourceindex).prepareSource(new Listener() {
            @Override
            public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
                handleSourceInfoRefreshed(index, timeline, manifest);
            }
        });
    }
}

@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
    for (int i = 0; i < mediaSources.size(); i++) {
        if (duplicateFlags.get(i) == null || !duplicateFlags.get(i)) {
            mediaSources.get(i).maybeThrowSourceInfoRefreshError();
        }
    }
}

@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
                                long positionUs) {
    int sourceIndex = timeline.getSourceIndexForPeriod(index);
    int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex);
    MediaPeriod mediaPeriod = mediaSources.get(sourceIndex).createPeriod(periodIndexInSource, callback,
            allocator, positionUs);
    sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
    return mediaPeriod;
}

@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
    int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod);
    sourceIndexByMediaPeriod.remove(mediaPeriod);
    mediaSources.get(sourceIndex).releasePeriod(mediaPeriod);
}

@Override
public void releaseSource() {
    for (int i = 0; i < mediaSources.size(); i++) {
        if (duplicateFlags.get(i) == null || !duplicateFlags.get(i)) {
            mediaSources.get(i).releaseSource();
        }
    }
}

private void handleSourceInfoRefreshed(int sourceFirstIndex, Timeline sourceTimeline,
                                       Object sourceManifest) {
    // Set the timeline and manifest.
    timelines.add(sourceFirstIndex, sourceTimeline);
    manifests.add(sourceFirstIndex, sourceManifest);
    // Also set the timeline and manifest for any duplicate entries of the same source.
    for (int i = sourceFirstIndex + 1; i < mediaSources.size(); i++) {
        if (mediaSources.get(i).equals(mediaSources.get(sourceFirstIndex))) {
            timelines.add(i, sourceTimeline);
            manifests.add(i, sourceManifest);
        }
    }
    for (Timeline timeline : timelines) {
        if (timeline == null) {
            // Don't invoke the listener until all sources have timelines.
            return;
        }
    }
    timeline = new DynamicTimeline(new ArrayList(timelines));
    listener.onSourceInfoRefreshed(timeline, new ArrayList(manifests));
}

private static SparseArray<Boolean> buildDuplicateFlags(List<MediaSource> mediaSources) {
    SparseArray<Boolean> duplicateFlags = new SparseArray<Boolean>();
    IdentityHashMap<MediaSource, Void> sources = new IdentityHashMap<>(mediaSources.size());
    for (int i = 0; i < mediaSources.size(); i++) {
        MediaSource mediaSource = mediaSources.get(i);
        if (!sources.containsKey(mediaSource)) {
            sources.put(mediaSource, null);
        } else {
            duplicateFlags.setValueAt(i, true);
        }
    }
    return duplicateFlags;
}

/**
 * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
 */
private static final class DynamicTimeline extends Timeline {

    private final List<Timeline> timelines;
    private final List<Integer> sourcePeriodOffsets;
    private final List<Integer> sourceWindowOffsets;

    public DynamicTimeline(List<Timeline> timelines) {
        List<Integer> sourcePeriodOffsets = new ArrayList<>();
        List<Integer> sourceWindowOffsets = new ArrayList<>();
        int periodCount = 0;
        int windowCount = 0;
        for (Timeline timeline : timelines) {
            periodCount += timeline.getPeriodCount();
            windowCount += timeline.getWindowCount();
            sourcePeriodOffsets.add(periodCount);
            sourceWindowOffsets.add(windowCount);
        }
        this.timelines = timelines;
        this.sourcePeriodOffsets = sourcePeriodOffsets;
        this.sourceWindowOffsets = sourceWindowOffsets;
    }

    @Override
    public int getWindowCount() {
        return sourceWindowOffsets.get(sourceWindowOffsets.size() - 1);
    }

    @Override
    public Window getWindow(int windowIndex, Window window, boolean setIds) {
        int sourceIndex = getSourceIndexForWindow(windowIndex);
        int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
        int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
        timelines.get(sourceIndex).getWindow(windowIndex - firstWindowIndexInSource, window, setIds);
        window.firstPeriodIndex += firstPeriodIndexInSource;
        window.lastPeriodIndex += firstPeriodIndexInSource;
        return window;
    }

    @Override
    public int getPeriodCount() {
        return sourcePeriodOffsets.get(sourcePeriodOffsets.size() - 1);
    }

    @Override
    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
        int sourceIndex = getSourceIndexForPeriod(periodIndex);
        int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
        int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
        timelines.get(sourceIndex).getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds);
        period.windowIndex += firstWindowIndexInSource;
        if (setIds) {
            period.uid = Pair.create(sourceIndex, period.uid);
        }
        return period;
    }

    @Override
    public int getIndexOfPeriod(Object uid) {
        if (!(uid instanceof Pair)) {
            return C.INDEX_UNSET;
        }
        Pair<?, ?> sourceIndexAndPeriodId = (Pair<?, ?>) uid;
        if (!(sourceIndexAndPeriodId.first instanceof Integer)) {
            return C.INDEX_UNSET;
        }
        int sourceIndex = (Integer) sourceIndexAndPeriodId.first;
        Object periodId = sourceIndexAndPeriodId.second;
        if (sourceIndex < 0 || sourceIndex >= timelines.size()) {
            return C.INDEX_UNSET;
        }
        int periodIndexInSource = timelines.get(sourceIndex).getIndexOfPeriod(periodId);
        return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET
                : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource;
    }

    private int getSourceIndexForPeriod(int periodIndex) {
        return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1;
    }

    private int getFirstPeriodIndexInSource(int sourceIndex) {
        return sourceIndex == 0 ? 0 : sourcePeriodOffsets.get(sourceIndex - 1);
    }

    private int getSourceIndexForWindow(int windowIndex) {
        return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1;
    }

    private int getFirstWindowIndexInSource(int sourceIndex) {
        return sourceIndex == 0 ? 0 : sourceWindowOffsets.get(sourceIndex - 1);
    }

}
}
like image 80
Manuela Avatar answered Oct 06 '22 00:10

Manuela