Skip to content

Commit

Permalink
Support multiple audio tracks in HLS.
Browse files Browse the repository at this point in the history
Issue: google#73
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=116808947
  • Loading branch information
ojw28 committed Mar 10, 2016
1 parent e81a1dc commit 7d5b17b
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
Expand Down Expand Up @@ -56,7 +57,8 @@
public class HlsRendererBuilder implements RendererBuilder {

private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int MAIN_BUFFER_SEGMENTS = 256;
private static final int MAIN_BUFFER_SEGMENTS = 254;
private static final int AUDIO_BUFFER_SEGMENTS = 54;
private static final int TEXT_BUFFER_SEGMENTS = 2;

private final Context context;
Expand Down Expand Up @@ -133,7 +135,15 @@ public void onSingleManifest(HlsPlaylist manifest) {
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();

// Build the video/audio/metadata renderers.
boolean haveSubtitles = false;
boolean haveAudios = false;
if (manifest instanceof HlsMasterPlaylist) {
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) manifest;
haveSubtitles = !masterPlaylist.subtitles.isEmpty();
haveAudios = !masterPlaylist.audios.isEmpty();
}

// Build the video/id3 renderers.
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
Expand All @@ -143,22 +153,34 @@ public void onSingleManifest(HlsPlaylist manifest) {
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
5000, mainHandler, player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
MetadataTrackRenderer<List<Id3Frame>> id3Renderer = new MetadataTrackRenderer<>(
sampleSource, new Id3Parser(), player, mainHandler.getLooper());

// Build the text renderer, preferring Webvtt where available.
boolean preferWebvtt = false;
if (manifest instanceof HlsMasterPlaylist) {
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
// Build the audio renderer.
MediaCodecAudioTrackRenderer audioRenderer;
if (haveAudios) {
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource audioChunkSource = new HlsChunkSource(false /* isMaster */, audioDataSource,
url, manifest, DefaultHlsTrackSelector.newAudioInstance(), bandwidthMeter,
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource audioSampleSource = new HlsSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT);
audioRenderer = new MediaCodecAudioTrackRenderer(
new SampleSource[] {sampleSource, audioSampleSource}, MediaCodecSelector.DEFAULT, null,
true, player.getMainHandler(), player, AudioCapabilities.getCapabilities(context),
AudioManager.STREAM_MUSIC);
} else {
audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
}

// Build the text renderer.
TrackRenderer textRenderer;
if (preferWebvtt) {
if (haveSubtitles) {
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
url, manifest, DefaultHlsTrackSelector.newSubtitleInstance(), bandwidthMeter,
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,32 @@ public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector medi
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) {
super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
this (new SampleSource[] {source}, mediaCodecSelector, drmSessionManager,
playClearSamplesWithoutKeys, eventHandler, eventListener, audioCapabilities, streamType);
}

/**
* @param sources The upstream sources from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public MediaCodecAudioTrackRenderer(SampleSource[] sources, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) {
super(sources, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener);
this.eventListener = eventListener;
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,28 @@ private static String buildCustomDiagnosticInfo(int errorCode) {
public MediaCodecTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener) {
super(source);
this (new SampleSource[] {source}, mediaCodecSelector, drmSessionManager,
playClearSamplesWithoutKeys, eventHandler, eventListener);
}

/**
* @param sources The upstream sources from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
* media is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public MediaCodecTrackRenderer(SampleSource[] sources, MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener) {
super(sources);
Assertions.checkState(Util.SDK_INT >= 16);
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
this.drmSessionManager = drmSessionManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ public MediaFormat copyWithDurationUs(long durationUs) {
encoderPadding);
}

public MediaFormat copyWithLanguage(String language) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language,
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, encoderDelay,
encoderPadding);
}

public MediaFormat copyWithFixedTrackInfo(String trackId, int bitrate, int width, int height,
String language) {
return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
public final class DefaultHlsTrackSelector implements HlsTrackSelector {

private static final int TYPE_DEFAULT = 0;
private static final int TYPE_VTT = 1;
private static final int TYPE_AUDIO = 1;
private static final int TYPE_SUBTITLE = 2;

private final Context context;
private final int type;
Expand All @@ -45,13 +46,22 @@ public static DefaultHlsTrackSelector newDefaultInstance(Context context) {
return new DefaultHlsTrackSelector(context, TYPE_DEFAULT);
}

/**
* Creates a {@link DefaultHlsTrackSelector} that selects alternate audio renditions.
*
* @return The selector instance.
*/
public static DefaultHlsTrackSelector newAudioInstance() {
return new DefaultHlsTrackSelector(null, TYPE_AUDIO);
}

/**
* Creates a {@link DefaultHlsTrackSelector} that selects subtitle renditions.
*
* @return The selector instance.
*/
public static DefaultHlsTrackSelector newVttInstance() {
return new DefaultHlsTrackSelector(null, TYPE_VTT);
public static DefaultHlsTrackSelector newSubtitleInstance() {
return new DefaultHlsTrackSelector(null, TYPE_SUBTITLE);
}

private DefaultHlsTrackSelector(Context context, int type) {
Expand All @@ -61,11 +71,11 @@ private DefaultHlsTrackSelector(Context context, int type) {

@Override
public void selectTracks(HlsMasterPlaylist playlist, Output output) throws IOException {
if (type == TYPE_VTT) {
List<Variant> subtitleVariants = playlist.subtitles;
if (subtitleVariants != null && !subtitleVariants.isEmpty()) {
for (int i = 0; i < subtitleVariants.size(); i++) {
output.fixedTrack(playlist, subtitleVariants.get(i));
if (type == TYPE_AUDIO || type == TYPE_SUBTITLE) {
List<Variant> variants = type == TYPE_AUDIO ? playlist.audios : playlist.subtitles;
if (variants != null && !variants.isEmpty()) {
for (int i = 0; i < variants.size(); i++) {
output.fixedTrack(playlist, variants.get(i));
}
}
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public HlsChunkSource(boolean isMaster, DataSource dataSource, String playlistUr
List<Variant> variants = new ArrayList<>();
variants.add(new Variant(playlistUrl, format));
masterPlaylist = new HlsMasterPlaylist(playlistUrl, variants,
Collections.<Variant>emptyList());
Collections.<Variant>emptyList(), Collections.<Variant>emptyList(), null, null);
}
}

Expand Down Expand Up @@ -308,6 +308,23 @@ public Variant getFixedTrackVariant(int index) {
return variants.length == 1 ? variants[0] : null;
}

/**
* Returns the language of the audio muxed into variants, or null if unknown.
*
* @return The language of the audio muxed into variants, or null if unknown.
*/
public String getMuxedAudioLanguage() {
return masterPlaylist.muxedAudioLanguage;
}

/**
* Returns the language of the captions muxed into variants, or null if unknown.
*
* @return The language of the captions muxed into variants, or null if unknown.
*/
public String getMuxedCaptionLanguage() {
return masterPlaylist.muxedCaptionLanguage;
}

/**
* Returns the currently selected track index.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@
public final class HlsMasterPlaylist extends HlsPlaylist {

public final List<Variant> variants;
public final List<Variant> audios;
public final List<Variant> subtitles;

public HlsMasterPlaylist(String baseUri, List<Variant> variants, List<Variant> subtitles) {
public final String muxedAudioLanguage;
public final String muxedCaptionLanguage;

public HlsMasterPlaylist(String baseUri, List<Variant> variants,
List<Variant> audios, List<Variant> subtitles, String muxedAudioLanguage,
String muxedCaptionLanguage) {
super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = Collections.unmodifiableList(variants);
this.audios = Collections.unmodifiableList(audios);
this.subtitles = Collections.unmodifiableList(subtitles);
this.muxedAudioLanguage = muxedAudioLanguage;
this.muxedCaptionLanguage = muxedCaptionLanguage;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
private static final String METHOD_ATTR = "METHOD";
private static final String URI_ATTR = "URI";
private static final String IV_ATTR = "IV";
private static final String INSTREAM_ID_ATTR = "INSTREAM-ID";

private static final String AUDIO_TYPE = "AUDIO";
private static final String VIDEO_TYPE = "VIDEO";
Expand Down Expand Up @@ -98,6 +99,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
Pattern.compile(LANGUAGE_ATTR + "=\"(.+?)\"");
private static final Pattern NAME_ATTR_REGEX =
Pattern.compile(NAME_ATTR + "=\"(.+?)\"");
private static final Pattern INSTREAM_ID_ATTR_REGEX =
Pattern.compile(INSTREAM_ID_ATTR + "=\"(.+?)\"");
// private static final Pattern AUTOSELECT_ATTR_REGEX =
// HlsParserUtil.compileBooleanAttrPattern(AUTOSELECT_ATTR);
// private static final Pattern DEFAULT_ATTR_REGEX =
Expand Down Expand Up @@ -140,29 +143,48 @@ public HlsPlaylist parse(String connectionUrl, InputStream inputStream)
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
throws IOException {
ArrayList<Variant> variants = new ArrayList<>();
ArrayList<Variant> audios = new ArrayList<>();
ArrayList<Variant> subtitles = new ArrayList<>();
int bitrate = 0;
String codecs = null;
int width = -1;
int height = -1;
String name = null;
String muxedAudioLanguage = null;
String muxedCaptionLanguage = null;

boolean expectingStreamInfUrl = false;
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(MEDIA_TAG)) {
String type = HlsParserUtil.parseStringAttr(line, TYPE_ATTR_REGEX, TYPE_ATTR);
if (SUBTITLES_TYPE.equals(type)) {
if (CLOSED_CAPTIONS_TYPE.equals(type)) {
String instreamId = HlsParserUtil.parseStringAttr(line, INSTREAM_ID_ATTR_REGEX,
INSTREAM_ID_ATTR);
if ("CC1".equals(instreamId)) {
muxedCaptionLanguage = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
}
} else if (SUBTITLES_TYPE.equals(type)) {
// We assume all subtitles belong to the same group.
String subtitleName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR);
String uri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR);
String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
Format format = new Format(subtitleName, MimeTypes.APPLICATION_M3U8, -1, -1, -1, -1, -1,
-1, language, codecs);
subtitles.add(new Variant(uri, format));
} else {
// TODO: Support other types of media tag.
} else if (AUDIO_TYPE.equals(type)) {
// We assume all audios belong to the same group.
String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
String uri = HlsParserUtil.parseOptionalStringAttr(line, URI_ATTR_REGEX);
if (uri != null) {
String audioName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR);
Format format = new Format(audioName, MimeTypes.APPLICATION_M3U8, -1, -1, -1, -1, -1,
-1, language, codecs);
audios.add(new Variant(uri, format));
} else {
muxedAudioLanguage = language;
}
}
} else if (line.startsWith(STREAM_INF_TAG)) {
bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
Expand Down Expand Up @@ -202,7 +224,8 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri
expectingStreamInfUrl = false;
}
}
return new HlsMasterPlaylist(baseUri, variants, subtitles);
return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioLanguage,
muxedCaptionLanguage);
}

private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
Expand Down
Loading

0 comments on commit 7d5b17b

Please sign in to comment.