Commit e770e5c2 by Oliver Woodman

Multi-track - The (nearly) final step.

- Migrate demo app to use new APIs.
- Add multi-track support for ExtractorSampleSource case.
- Add multi-track support for SmoothStreaming use case.

The final step is to add support back for the DASH use case and
delete MultiTrackChunkSource. This is blocked on multi-period support
landing, in order to prevent a horrendous merge conflict. We also
need to update HLS to expose sensible track information.

Issue: #514
parent 57250036
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer.demo;
import com.google.android.exoplayer.AspectRatioFrameLayout;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
import com.google.android.exoplayer.demo.player.DashRendererBuilder;
......@@ -33,6 +34,7 @@ import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.SubtitleLayout;
import com.google.android.exoplayer.util.DebugTextViewHelper;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import com.google.android.exoplayer.util.VerboseLogUtil;
......@@ -435,23 +437,34 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
});
Menu menu = popup.getMenu();
// ID_OFFSET ensures we avoid clashing with Menu.NONE (which equals 0)
menu.add(MENU_GROUP_TRACKS, DemoPlayer.DISABLED_TRACK + ID_OFFSET, Menu.NONE, R.string.off);
if (trackCount == 1 && TextUtils.isEmpty(player.getTrackName(trackType, 0))) {
menu.add(MENU_GROUP_TRACKS, DemoPlayer.PRIMARY_TRACK + ID_OFFSET, Menu.NONE, R.string.on);
} else {
for (int i = 0; i < trackCount; i++) {
menu.add(MENU_GROUP_TRACKS, i + ID_OFFSET, Menu.NONE, player.getTrackName(trackType, i));
}
menu.add(MENU_GROUP_TRACKS, DemoPlayer.TRACK_DISABLED + ID_OFFSET, Menu.NONE, R.string.off);
for (int i = 0; i < trackCount; i++) {
menu.add(MENU_GROUP_TRACKS, i + ID_OFFSET, Menu.NONE,
buildTrackName(player.getTrackFormat(trackType, i)));
}
menu.setGroupCheckable(MENU_GROUP_TRACKS, true, true);
menu.findItem(player.getSelectedTrackIndex(trackType) + ID_OFFSET).setChecked(true);
menu.findItem(player.getSelectedTrack(trackType) + ID_OFFSET).setChecked(true);
}
private static String buildTrackName(MediaFormat format) {
if (format.adaptive) {
return "auto";
} else if (MimeTypes.isVideo(format.mimeType)) {
return format.width + "x" + format.height;
} else if (MimeTypes.isAudio(format.mimeType)) {
return format.channelCount + "ch, " + format.sampleRate + "Hz";
} else if (MimeTypes.isText(format.mimeType) && !TextUtils.isEmpty(format.language)) {
return format.language;
} else {
return "unknown";
}
}
private boolean onTrackItemClick(MenuItem item, int type) {
if (player == null || item.getGroupId() != MENU_GROUP_TRACKS) {
return false;
}
player.selectTrack(type, item.getItemId() - ID_OFFSET);
player.setSelectedTrack(type, item.getItemId() - ID_OFFSET);
return true;
}
......
......@@ -352,7 +352,7 @@ public class DashRendererBuilder implements RendererBuilder {
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(trackNames, multiTrackChunkSources, renderers, bandwidthMeter);
player.onRenderers(renderers, bandwidthMeter);
}
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
......
......@@ -23,12 +23,12 @@ import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.hls.HlsSampleSource;
......@@ -46,7 +46,6 @@ import android.os.Looper;
import android.view.Surface;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
......@@ -148,9 +147,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
public static final int STATE_READY = ExoPlayer.STATE_READY;
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
public static final int DISABLED_TRACK = -1;
public static final int PRIMARY_TRACK = 0;
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
public static final int RENDERER_COUNT = 4;
public static final int TYPE_VIDEO = 0;
......@@ -179,9 +177,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private int videoTrackToRestore;
private BandwidthMeter bandwidthMeter;
private MultiTrackChunkSource[] multiTrackSources;
private String[][] trackNames;
private int[] selectedTracks;
private boolean backgrounded;
private CaptionListener captionListener;
......@@ -198,9 +193,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
listeners = new CopyOnWriteArrayList<>();
lastReportedPlaybackState = STATE_IDLE;
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
selectedTracks = new int[RENDERER_COUNT];
// Disable text initially.
selectedTracks[TYPE_TEXT] = DISABLED_TRACK;
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
}
public PlayerControl getPlayerControl() {
......@@ -245,28 +239,20 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
pushSurface(true);
}
@SuppressWarnings("deprecation")
public int getTrackCount(int type) {
return !player.getRendererHasMedia(type) ? 0 : trackNames[type].length;
return player.getTrackCount(type);
}
public String getTrackName(int type, int index) {
return trackNames[type][index];
public MediaFormat getTrackFormat(int type, int index) {
return player.getTrackFormat(type, index);
}
public int getSelectedTrackIndex(int type) {
return selectedTracks[type];
public int getSelectedTrack(int type) {
return player.getSelectedTrack(type);
}
public void selectTrack(int type, int index) {
if (selectedTracks[type] == index) {
return;
}
selectedTracks[type] = index;
pushTrackSelection(type, true);
if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) {
captionListener.onCues(Collections.<Cue>emptyList());
}
public void setSelectedTrack(int type, int index) {
player.setSelectedTrack(type, index);
}
public boolean getBackgrounded() {
......@@ -279,11 +265,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
this.backgrounded = backgrounded;
if (backgrounded) {
videoTrackToRestore = getSelectedTrackIndex(TYPE_VIDEO);
selectTrack(TYPE_VIDEO, DISABLED_TRACK);
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
blockingClearSurface();
} else {
selectTrack(TYPE_VIDEO, videoTrackToRestore);
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
}
}
......@@ -294,7 +280,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
rendererBuilder.cancel();
videoFormat = null;
videoRenderer = null;
multiTrackSources = null;
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
maybeReportPlayerState();
rendererBuilder.buildRenderers(this);
......@@ -303,51 +288,25 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
/**
* Invoked with the results from a {@link RendererBuilder}.
*
* @param trackNames The names of the available tracks, indexed by {@link DemoPlayer} TYPE_*
* constants. May be null if the track names are unknown. An individual element may be null
* if the track names are unknown for the corresponding type.
* @param multiTrackSources Sources capable of switching between multiple available tracks,
* indexed by {@link DemoPlayer} TYPE_* constants. May be null if there are no types with
* multiple tracks. An individual element may be null if it does not have multiple tracks.
* @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual
* element may be null if there do not exist tracks of the corresponding type.
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
*/
/* package */ void onRenderers(String[][] trackNames,
MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers,
BandwidthMeter bandwidthMeter) {
// Normalize the results.
if (trackNames == null) {
trackNames = new String[RENDERER_COUNT][];
}
if (multiTrackSources == null) {
multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT];
}
for (int rendererIndex = 0; rendererIndex < RENDERER_COUNT; rendererIndex++) {
if (renderers[rendererIndex] == null) {
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
for (int i = 0; i < RENDERER_COUNT; i++) {
if (renderers[i] == null) {
// Convert a null renderer to a dummy renderer.
renderers[rendererIndex] = new DummyTrackRenderer();
}
if (trackNames[rendererIndex] == null) {
// Convert a null trackNames to an array of suitable length.
int trackCount = multiTrackSources[rendererIndex] != null
? multiTrackSources[rendererIndex].getMultiTrackCount() : 1;
trackNames[rendererIndex] = new String[trackCount];
renderers[i] = new DummyTrackRenderer();
}
}
// Complete preparation.
this.trackNames = trackNames;
this.videoRenderer = renderers[TYPE_VIDEO];
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
this.multiTrackSources = multiTrackSources;
this.bandwidthMeter = bandwidthMeter;
pushSurface(false);
pushTrackSelection(TYPE_VIDEO, true);
pushTrackSelection(TYPE_AUDIO, true);
pushTrackSelection(TYPE_TEXT, true);
player.prepare(renderers);
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
}
......@@ -537,14 +496,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override
public void onCues(List<Cue> cues) {
if (captionListener != null && selectedTracks[TYPE_TEXT] != DISABLED_TRACK) {
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
captionListener.onCues(cues);
}
}
@Override
public void onMetadata(Map<String, Object> metadata) {
if (id3MetadataListener != null && selectedTracks[TYPE_METADATA] != DISABLED_TRACK) {
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
id3MetadataListener.onId3Metadata(metadata);
}
}
......@@ -620,26 +579,4 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
}
@SuppressWarnings("deprecation")
private void pushTrackSelection(int type, boolean allowRendererEnable) {
if (multiTrackSources == null) {
return;
}
int trackIndex = selectedTracks[type];
if (trackIndex == DISABLED_TRACK) {
player.setRendererEnabled(type, false);
} else if (multiTrackSources[type] == null) {
player.setRendererEnabled(type, allowRendererEnable);
} else {
boolean playWhenReady = player.getPlayWhenReady();
player.setPlayWhenReady(false);
player.setRendererEnabled(type, false);
player.sendMessage(multiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK,
trackIndex);
player.setRendererEnabled(type, allowRendererEnable);
player.setPlayWhenReady(playWhenReady);
}
}
}
......@@ -74,7 +74,7 @@ public class ExtractorRendererBuilder implements RendererBuilder {
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(null, null, renderers, bandwidthMeter);
player.onRenderers(renderers, bandwidthMeter);
}
@Override
......
......@@ -162,7 +162,7 @@ public class HlsRendererBuilder implements RendererBuilder {
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_METADATA] = id3Renderer;
renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer;
player.onRenderers(null, null, renderers, bandwidthMeter);
player.onRenderers(renderers, bandwidthMeter);
}
}
......
......@@ -28,6 +28,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import java.io.IOException;
import java.util.HashMap;
/**
......@@ -42,7 +43,7 @@ public final class MediaCodecUtil {
* Such failures are not expected in normal operation and are normally temporary (e.g. if the
* mediaserver process has crashed and is yet to restart).
*/
public static class DecoderQueryException extends Exception {
public static class DecoderQueryException extends IOException {
private DecoderQueryException(Throwable cause) {
super("Failed to query underlying media codecs", cause);
......
......@@ -187,15 +187,6 @@ public final class MediaFormat {
NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, false, NO_VALUE, NO_VALUE);
}
public static MediaFormat createAdaptiveFormat(String mimeType) {
return createAdaptiveFormat(mimeType, C.UNKNOWN_TIME_US);
}
public static MediaFormat createAdaptiveFormat(String mimeType, long durationUs) {
return new MediaFormat(mimeType, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, true, NO_VALUE, NO_VALUE);
}
/* package */ MediaFormat(String mimeType, int maxInputSize, long durationUs, int width,
int height, int rotationDegrees, float pixelWidthHeightRatio, int channelCount,
int sampleRate, String language, long subsampleOffsetUs, List<byte[]> initializationData,
......@@ -236,6 +227,12 @@ public final class MediaFormat {
initializationData, adaptive, maxWidth, maxHeight);
}
public MediaFormat copyWithAdaptive(boolean adaptive) {
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, rotationDegrees,
pixelWidthHeightRatio, channelCount, sampleRate, language, subsampleOffsetUs,
initializationData, adaptive, maxWidth, maxHeight);
}
/**
* @return A {@link MediaFormat} representation of this format.
*/
......
......@@ -134,7 +134,9 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
} else if (!chunkSource.prepare()) {
return false;
}
loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType);
if (chunkSource.getTrackCount() > 0) {
loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType);
}
state = STATE_PREPARED;
return true;
}
......
......@@ -67,11 +67,21 @@ public interface ChunkSource {
MediaFormat getFormat(int track);
/**
* Enable the source for the specified track.
* <p>
* This method should only be called after the source has been prepared, and when the source is
* disabled.
*
* @param track The track index.
*/
void enable(int track);
/**
* Adaptive video {@link ChunkSource} implementations must return a copy of the provided
* {@link MediaFormat} with the maximum video dimensions set. Other implementations can return
* the provided {@link MediaFormat} directly.
* <p>
* This method should only be called after the source has been prepared.
* This method should only be called when the source is enabled.
*
* @param format The format to be copied or returned.
* @return A copy of the provided {@link MediaFormat} with the maximum video dimensions set, or
......@@ -80,16 +90,6 @@ public interface ChunkSource {
MediaFormat getWithMaxVideoDimensions(MediaFormat format);
/**
* Enable the source for the specified track.
* <p>
* This method should only be called after the source has been prepared, and when the source is
* disabled.
*
* @param track The track index.
*/
void enable(int track);
/**
* Indicates to the source that it should still be checking for updates to the stream.
* <p>
* This method should only be called when the source is enabled.
......
/*
* Copyright (C) 2014 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 com.google.android.exoplayer.smoothstreaming;
import java.io.IOException;
/**
* Specifies a track selection from a {@link SmoothStreamingManifest}.
*/
public interface SmoothStreamingTrackSelector {
/**
* Defines a selector output.
*/
interface Output {
/**
* Outputs an adaptive track, covering the specified tracks in the specified element.
*
* @param manifest The manifest being processed.
* @param element The index of the element within which the adaptive tracks are located.
* @param tracks The indices of the tracks within the element.
*/
void adaptiveTrack(SmoothStreamingManifest manifest, int element, int[] tracks);
/**
* Outputs a fixed track corresponding to the specified track in the specified element.
*
* @param manifest The manifest being processed.
* @param element The index of the element within which the adaptive tracks are located.
* @param track The index of the track within the element.
*/
void fixedTrack(SmoothStreamingManifest manifest, int element, int track);
}
/**
* Outputs a track selection for a given manifest.
*
* @param manifest The manifest to process.
* @param output The output to receive tracks.
* @throws IOException If an error occurs processing the manifest.
*/
void selectTracks(SmoothStreamingManifest manifest, Output output) throws IOException;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment