/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
/**
* Merges multiple {@link MediaSource}s.
*
*
The {@link Timeline}s of the sources being merged must have the same number of periods.
*/
public final class MergingMediaSource extends CompositeMediaSource {
/**
* Thrown when a {@link MergingMediaSource} cannot merge its sources.
*/
public static final class IllegalMergeException extends IOException {
/** The reason the merge failed. One of {@link #REASON_PERIOD_COUNT_MISMATCH}. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({REASON_PERIOD_COUNT_MISMATCH})
public @interface Reason {}
/**
* The sources have different period counts.
*/
public static final int REASON_PERIOD_COUNT_MISMATCH = 0;
/**
* The reason the merge failed.
*/
@Reason public final int reason;
/**
* @param reason The reason the merge failed.
*/
public IllegalMergeException(@Reason int reason) {
this.reason = reason;
}
}
private static final int PERIOD_COUNT_UNSET = -1;
private final MediaSource[] mediaSources;
private final Timeline[] timelines;
private final ArrayList pendingTimelineSources;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private int periodCount;
@Nullable private IllegalMergeException mergeError;
/**
* @param mediaSources The {@link MediaSource}s to merge.
*/
public MergingMediaSource(MediaSource... mediaSources) {
this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources);
}
/**
* @param compositeSequenceableLoaderFactory A factory to create composite
* {@link SequenceableLoader}s for when this media source loads data from multiple streams
* (video, audio etc...).
* @param mediaSources The {@link MediaSource}s to merge.
*/
public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
MediaSource... mediaSources) {
this.mediaSources = mediaSources;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
periodCount = PERIOD_COUNT_UNSET;
timelines = new Timeline[mediaSources.length];
}
@Override
@Nullable
public Object getTag() {
return mediaSources.length > 0 ? mediaSources[0].getTag() : null;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
for (int i = 0; i < mediaSources.length; i++) {
prepareChildSource(i, mediaSources[i]);
}
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (mergeError != null) {
throw mergeError;
}
super.maybeThrowSourceInfoRefreshError();
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
for (int i = 0; i < periods.length; i++) {
MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);
}
return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
for (int i = 0; i < mediaSources.length; i++) {
mediaSources[i].releasePeriod(mergingPeriod.periods[i]);
}
}
@Override
protected void releaseSourceInternal() {
super.releaseSourceInternal();
Arrays.fill(timelines, null);
periodCount = PERIOD_COUNT_UNSET;
mergeError = null;
pendingTimelineSources.clear();
Collections.addAll(pendingTimelineSources, mediaSources);
}
@Override
protected void onChildSourceInfoRefreshed(
Integer id, MediaSource mediaSource, Timeline timeline) {
if (mergeError == null) {
mergeError = checkTimelineMerges(timeline);
}
if (mergeError != null) {
return;
}
pendingTimelineSources.remove(mediaSource);
timelines[id] = timeline;
if (pendingTimelineSources.isEmpty()) {
refreshSourceInfo(timelines[0]);
}
}
@Override
@Nullable
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
Integer id, MediaPeriodId mediaPeriodId) {
return id == 0 ? mediaPeriodId : null;
}
@Nullable
private IllegalMergeException checkTimelineMerges(Timeline timeline) {
if (periodCount == PERIOD_COUNT_UNSET) {
periodCount = timeline.getPeriodCount();
} else if (timeline.getPeriodCount() != periodCount) {
return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);
}
return null;
}
}