androidexoplayerplaylistsexoplayer2.x

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?


Solution

  • 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);
        }
    
    }
    }