Merge pull request #267 from google/dev-hls

Merge dev-hls -> dev
This commit is contained in:
Andrey Udovenko 2015-01-27 11:48:15 -05:00
commit 27c0e7d776
33 changed files with 4845 additions and 22 deletions

View File

@ -50,6 +50,7 @@ public class DemoUtil {
public static final int TYPE_DASH = 0;
public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2;
public static final int TYPE_HLS = 3;
public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false;

View File

@ -56,6 +56,8 @@ public class SampleChooserActivity extends Activity {
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
sampleAdapter.add(new Header("Misc"));
sampleAdapter.addAll((Object[]) Samples.MISC);
sampleAdapter.add(new Header("HLS"));
sampleAdapter.addAll((Object[]) Samples.HLS);
if (DemoUtil.EXPOSE_EXPERIMENTAL_FEATURES) {
sampleAdapter.add(new Header("YouTube WebM DASH (Experimental)"));
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM);

View File

@ -52,6 +52,9 @@ package com.google.android.exoplayer.demo;
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS, false),
new Sample("Apple master playlist (HLS)", "uid:hls:applemaster",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
+ "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, false),
new Sample("Dizzy (Misc)", "uid:misc:dizzy",
"http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false),
};
@ -124,6 +127,18 @@ package com.google.android.exoplayer.demo;
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true),
};
public static final Sample[] HLS = new Sample[] {
new Sample("Apple master playlist", "uid:hls:applemaster",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
+ "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, true),
new Sample("Apple master playlist advanced", "uid:hls:applemasteradvanced",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/"
+ "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS, true),
new Sample("Apple single media playlist", "uid:hls:applesinglemedia",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/"
+ "prog_index.m3u8", DemoUtil.TYPE_HLS, true),
};
public static final Sample[] MISC = new Sample[] {
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
DemoUtil.TYPE_OTHER, true),

View File

@ -25,8 +25,10 @@ import com.google.android.exoplayer.demo.full.player.DashRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.demo.full.player.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.TxxxMetadata;
import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView;
import com.google.android.exoplayer.util.Util;
@ -40,6 +42,7 @@ import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;
import android.view.Menu;
import android.view.MenuItem;
@ -57,11 +60,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Map;
/**
* An activity that plays media using {@link DemoPlayer}.
*/
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
DemoPlayer.Listener, DemoPlayer.TextListener, AudioCapabilitiesReceiver.Listener {
DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener,
AudioCapabilitiesReceiver.Listener {
private static final String TAG = "FullPlayerActivity";
private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f;
private static final int MENU_GROUP_TRACKS = 1;
@ -199,6 +207,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId);
default:
return new DefaultRendererBuilder(this, contentUri, debugTextView);
}
@ -209,6 +219,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
player = new DemoPlayer(getRendererBuilder());
player.addListener(this);
player.setTextListener(this);
player.setMetadataListener(this);
player.seekTo(playerPosition);
playerNeedsPrepare = true;
mediaController.setMediaPlayer(player.getPlayerControl());
@ -435,6 +446,19 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
}
}
// DemoPlayer.MetadataListener implementation
@Override
public void onId3Metadata(Map<String, Object> metadata) {
for (int i = 0; i < metadata.size(); i++) {
if (metadata.containsKey(TxxxMetadata.TYPE)) {
TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE);
Log.i(TAG, String.format("ID3 TimedMetadata: description=%s, value=%s",
txxxMetadata.description, txxxMetadata.value));
}
}
}
// SurfaceHolder.Callback implementation
@Override

View File

@ -27,7 +27,8 @@ import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.util.PlayerControl;
@ -37,6 +38,7 @@ import android.os.Looper;
import android.view.Surface;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@ -47,7 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener,
TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener {
TextRenderer, StreamingDrmSessionManager.EventListener {
/**
* Builds renderers for the player.
@ -136,6 +138,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onText(String text);
}
/**
* A listener for receiving ID3 metadata parsed from the media stream.
*/
public interface Id3MetadataListener {
void onId3Metadata(Map<String, Object> metadata);
}
// Constants pulled into this class for convenience.
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
@ -146,11 +155,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public static final int DISABLED_TRACK = -1;
public static final int PRIMARY_TRACK = 0;
public static final int RENDERER_COUNT = 4;
public static final int RENDERER_COUNT = 5;
public static final int TYPE_VIDEO = 0;
public static final int TYPE_AUDIO = 1;
public static final int TYPE_TEXT = 2;
public static final int TYPE_DEBUG = 3;
public static final int TYPE_TIMED_METADATA = 3;
public static final int TYPE_DEBUG = 4;
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
@ -175,6 +185,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private int[] selectedTracks;
private TextListener textListener;
private Id3MetadataListener id3MetadataListener;
private InternalErrorListener internalErrorListener;
private InfoListener infoListener;
@ -216,6 +227,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
textListener = listener;
}
public void setMetadataListener(Id3MetadataListener listener) {
id3MetadataListener = listener;
}
public void setSurface(Surface surface) {
this.surface = surface;
pushSurfaceAndVideoTrack(false);
@ -465,9 +480,19 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override
public void onText(String text) {
if (textListener != null) {
textListener.onText(text);
processText(text);
}
/* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>
getId3MetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() {
@Override
public void onMetadata(Map<String, Object> metadata) {
if (id3MetadataListener != null) {
id3MetadataListener.onId3Metadata(metadata);
}
}
};
}
@Override
@ -561,6 +586,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
}
/* package */ void processText(String text) {
if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) {
return;
}
textListener.onText(text);
}
private class InternalRendererBuilderCallback implements RendererBuilderCallback {
private boolean canceled;

View File

@ -0,0 +1,101 @@
/*
* 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.demo.full.player;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback;
import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsPlaylist;
import com.google.android.exoplayer.hls.HlsPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.Id3Parser;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.media.MediaCodec;
import java.io.IOException;
import java.util.Map;
/**
* A {@link RendererBuilder} for HLS.
*/
public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<HlsPlaylist> {
private final String userAgent;
private final String url;
private final String contentId;
private DemoPlayer player;
private RendererBuilderCallback callback;
public HlsRendererBuilder(String userAgent, String url, String contentId) {
this.userAgent = userAgent;
this.url = url;
this.contentId = contentId;
}
@Override
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
this.player = player;
this.callback = callback;
HlsPlaylistParser parser = new HlsPlaylistParser();
ManifestFetcher<HlsPlaylist> playlistFetcher =
new ManifestFetcher<HlsPlaylist>(parser, contentId, url, userAgent);
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
@Override
public void onManifestError(String contentId, IOException e) {
callback.onRenderersError(e);
}
@Override
public void onManifest(String contentId, HlsPlaylist manifest) {
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, null,
HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, player.getMainHandler(), player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
MetadataTrackRenderer<Map<String, Object>> id3Renderer =
new MetadataTrackRenderer<Map<String, Object>>(sampleSource, new Id3Parser(),
player.getId3MetadataRenderer(), player.getMainHandler().getLooper());
Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player,
player.getMainHandler().getLooper());
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TIMED_METADATA] = id3Renderer;
renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer;
callback.onRenderers(null, null, renderers);
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.demo.simple;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsPlaylist;
import com.google.android.exoplayer.hls.HlsPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.media.MediaCodec;
import java.io.IOException;
/**
* A {@link RendererBuilder} for HLS.
*/
/* package */ class HlsRendererBuilder implements RendererBuilder,
ManifestCallback<HlsPlaylist> {
private final SimplePlayerActivity playerActivity;
private final String userAgent;
private final String url;
private final String contentId;
private RendererBuilderCallback callback;
public HlsRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
String contentId) {
this.playerActivity = playerActivity;
this.userAgent = userAgent;
this.url = url;
this.contentId = contentId;
}
@Override
public void buildRenderers(RendererBuilderCallback callback) {
this.callback = callback;
HlsPlaylistParser parser = new HlsPlaylistParser();
ManifestFetcher<HlsPlaylist> playlistFetcher =
new ManifestFetcher<HlsPlaylist>(parser, contentId, url, userAgent);
playlistFetcher.singleLoad(playerActivity.getMainLooper(), this);
}
@Override
public void onManifestError(String contentId, IOException e) {
callback.onRenderersError(e);
}
@Override
public void onManifest(String contentId, HlsPlaylist manifest) {
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, null,
HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(),
playerActivity, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
callback.onRenderers(videoRenderer, audioRenderer);
}
}

View File

@ -166,6 +166,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
contentId);
case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId);
default:
return new DefaultRendererBuilder(this, contentUri);
}

View File

@ -87,6 +87,14 @@ public class MediaFormat {
sampleRate, bitrate, initializationData);
}
public static MediaFormat createId3Format() {
return createFormatForMimeType(MimeTypes.APPLICATION_ID3);
}
public static MediaFormat createEia608Format() {
return createFormatForMimeType(MimeTypes.APPLICATION_EIA608);
}
public static MediaFormat createTtmlFormat() {
return createFormatForMimeType(MimeTypes.APPLICATION_TTML);
}

View File

@ -0,0 +1,100 @@
/*
* 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.hls;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.BitArray;
import java.io.IOException;
/**
* An abstract base class for {@link HlsChunk} implementations where the data should be loaded into
* a {@link BitArray} and subsequently consumed.
*/
public abstract class BitArrayChunk extends HlsChunk {
private static final int READ_GRANULARITY = 16 * 1024;
private final BitArray bitArray;
private volatile boolean loadFinished;
private volatile boolean loadCanceled;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
* @param bitArray The {@link BitArray} into which the data should be loaded.
*/
public BitArrayChunk(DataSource dataSource, DataSpec dataSpec, BitArray bitArray) {
super(dataSource, dataSpec);
this.bitArray = bitArray;
}
@Override
public void consume() throws IOException {
consume(bitArray);
}
/**
* Invoked by {@link #consume()}. Implementations should override this method to consume the
* loaded data.
*
* @param bitArray The {@link BitArray} containing the loaded data.
* @throws IOException If an error occurs consuming the loaded data.
*/
protected abstract void consume(BitArray bitArray) throws IOException;
/**
* Whether the whole of the chunk has been loaded.
*
* @return True if the whole of the chunk has been loaded. False otherwise.
*/
@Override
public boolean isLoadFinished() {
return loadFinished;
}
// Loadable implementation
@Override
public final void cancelLoad() {
loadCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public final void load() throws IOException, InterruptedException {
try {
bitArray.reset();
dataSource.open(dataSpec);
int bytesRead = 0;
while (bytesRead != -1 && !loadCanceled) {
bytesRead = bitArray.append(dataSource, READ_GRANULARITY);
}
loadFinished = !loadCanceled;
} finally {
dataSource.close();
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.hls;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
/**
* An abstract base class for {@link Loadable} implementations that load chunks of data required
* for the playback of HLS streams.
*/
public abstract class HlsChunk implements Loadable {
protected final DataSource dataSource;
protected final DataSpec dataSpec;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
*/
public HlsChunk(DataSource dataSource, DataSpec dataSpec) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
}
public abstract void consume() throws IOException;
public abstract boolean isLoadFinished();
}

View File

@ -0,0 +1,573 @@
/*
* 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.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.TsExtractor.SamplePool;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* A temporary test source of HLS chunks.
* <p>
* TODO: Figure out whether this should merge with the chunk package, or whether the hls
* implementation is going to naturally diverge.
*/
public class HlsChunkSource {
/**
* Adaptive switching is disabled.
* <p>
* The initially selected variant will be used throughout playback.
*/
public static final int ADAPTIVE_MODE_NONE = 0;
/**
* Adaptive switches splice overlapping segments of the old and new variants.
* <p>
* When performing a switch from one variant to another, overlapping segments will be requested
* from both the old and new variants. These segments will then be spliced together, allowing
* a seamless switch from one variant to another even if keyframes are misaligned or if keyframes
* are not positioned at the start of each segment.
* <p>
* Note that where it can be guaranteed that the source content has keyframes positioned at the
* start of each segment, {@link #ADAPTIVE_MODE_ABRUPT} should always be used in preference to
* this mode.
*/
public static final int ADAPTIVE_MODE_SPLICE = 1;
/**
* Adaptive switches are performed at segment boundaries.
* <p>
* For this mode to perform seamless switches, the source content is required to have keyframes
* positioned at the start of each segment. If this is not the case a visual discontinuity may
* be experienced when switching from one variant to another.
* <p>
* Note that where it can be guaranteed that the source content does have keyframes positioned at
* the start of each segment, this mode should always be used in preference to
* {@link #ADAPTIVE_MODE_SPLICE} because it requires fetching less data.
*/
public static final int ADAPTIVE_MODE_ABRUPT = 3;
/**
* The default target buffer duration in milliseconds.
*/
public static final long DEFAULT_TARGET_BUFFER_DURATION_MS = 40000;
/**
* The default minimum duration of media that needs to be buffered for a switch to a higher
* quality variant to be considered.
*/
public static final long DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS = 5000;
/**
* The default maximum duration of media that needs to be buffered for a switch to a lower
* quality variant to be considered.
*/
public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
private static final String TAG = "HlsChunkSource";
private static final float BANDWIDTH_FRACTION = 0.8f;
private final SamplePool samplePool = new TsExtractor.SamplePool();
private final DataSource upstreamDataSource;
private final HlsPlaylistParser playlistParser;
private final Variant[] enabledVariants;
private final BandwidthMeter bandwidthMeter;
private final BitArray bitArray;
private final int adaptiveMode;
private final Uri baseUri;
private final int maxWidth;
private final int maxHeight;
private final long targetBufferDurationUs;
private final long minBufferDurationToSwitchUpUs;
private final long maxBufferDurationToSwitchDownUs;
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ final boolean[] mediaPlaylistBlacklistFlags;
/* package */ final long[] lastMediaPlaylistLoadTimesMs;
/* package */ boolean live;
/* package */ long durationUs;
private int variantIndex;
private DataSource encryptedDataSource;
private Uri encryptionKeyUri;
private String encryptedDataSourceIv;
private byte[] encryptedDataSourceSecretKey;
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode,
DEFAULT_TARGET_BUFFER_DURATION_MS, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS,
DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS);
}
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param playlistUrl The playlist URL.
* @param playlist The hls playlist.
* @param bandwidthMeter provides an estimate of the currently available bandwidth.
* @param variantIndices A subset of variant indices to consider, or null to consider all of the
* variants in the master playlist.
* @param adaptiveMode The mode for switching from one variant to another. One of
* {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
* {@link #ADAPTIVE_MODE_SPLICE}.
* @param targetBufferDurationMs The targeted duration of media to buffer ahead of the current
* playback position. Note that the greater this value, the greater the amount of memory
* that will be consumed.
* @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered
* for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
* for a switch to a lower quality variant to be considered.
*/
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
long targetBufferDurationMs, long minBufferDurationToSwitchUpMs,
long maxBufferDurationToSwitchDownMs) {
this.upstreamDataSource = dataSource;
this.bandwidthMeter = bandwidthMeter;
this.adaptiveMode = adaptiveMode;
targetBufferDurationUs = targetBufferDurationMs * 1000;
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
baseUri = playlist.baseUri;
bitArray = new BitArray();
playlistParser = new HlsPlaylistParser();
if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
mediaPlaylists = new HlsMediaPlaylist[1];
mediaPlaylistBlacklistFlags = new boolean[1];
lastMediaPlaylistLoadTimesMs = new long[1];
setMediaPlaylist(0, (HlsMediaPlaylist) playlist);
} else {
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER);
enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices);
mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length];
mediaPlaylistBlacklistFlags = new boolean[enabledVariants.length];
lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length];
}
int maxWidth = -1;
int maxHeight = -1;
// Select the first variant from the master playlist that's enabled.
long minOriginalVariantIndex = Integer.MAX_VALUE;
for (int i = 0; i < enabledVariants.length; i++) {
if (enabledVariants[i].index < minOriginalVariantIndex) {
minOriginalVariantIndex = enabledVariants[i].index;
variantIndex = i;
}
maxWidth = Math.max(enabledVariants[i].width, maxWidth);
maxHeight = Math.max(enabledVariants[i].height, maxHeight);
}
// TODO: We should allow the default values to be passed through the constructor.
this.maxWidth = maxWidth > 0 ? maxWidth : 1920;
this.maxHeight = maxHeight > 0 ? maxHeight : 1080;
}
public long getDurationUs() {
return live ? C.UNKNOWN_TIME_US : durationUs;
}
/**
* Adaptive implementations must set the maximum video dimensions on the supplied
* {@link MediaFormat}. Other implementations do nothing.
* <p>
* Only called when the source is enabled.
*
* @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
*/
public void getMaxVideoDimensions(MediaFormat out) {
out.setMaxVideoDimensions(maxWidth, maxHeight);
}
/**
* Returns the next {@link HlsChunk} that should be loaded.
*
* @param previousTsChunk The previously loaded chunk that the next chunk should follow.
* @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
* position. If there is a previous chunk then this parameter is ignored.
* @param playbackPositionUs The current playback position.
* @return The next chunk to load.
*/
public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) {
if (previousTsChunk != null && (previousTsChunk.isLastChunk
|| previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)) {
// We're either finished, or we have the target amount of data buffered.
return null;
}
int nextVariantIndex = variantIndex;
boolean switchingVariant = false;
boolean switchingVariantSpliced = false;
if (adaptiveMode == ADAPTIVE_MODE_NONE) {
// Do nothing.
} else {
nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
switchingVariant = nextVariantIndex != variantIndex;
switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE;
}
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[nextVariantIndex];
if (mediaPlaylist == null) {
// We don't have the media playlist for the next variant. Request it now.
return newMediaPlaylistChunk(nextVariantIndex);
}
variantIndex = nextVariantIndex;
int chunkMediaSequence = 0;
boolean liveDiscontinuity = false;
if (live) {
if (previousTsChunk == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
} else {
chunkMediaSequence = switchingVariantSpliced
? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
// If the chunk is no longer in the playlist. Skip ahead and start again.
chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
liveDiscontinuity = true;
}
}
} else {
// Not live.
if (previousTsChunk == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
true) + mediaPlaylist.mediaSequence;
} else {
chunkMediaSequence = switchingVariantSpliced
? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1;
}
}
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
if (chunkIndex >= mediaPlaylist.segments.size()) {
if (mediaPlaylist.live && shouldRerequestMediaPlaylist(variantIndex)) {
return newMediaPlaylistChunk(variantIndex);
} else {
return null;
}
}
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
Uri chunkUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.url);
// Check if encryption is specified.
if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) {
Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed.
HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
return toReturn;
}
if (!Util.areEqual(segment.encryptionIV, encryptedDataSourceIv)) {
initEncryptedDataSource(keyUri, segment.encryptionIV, encryptedDataSourceSecretKey);
}
} else {
clearEncryptedDataSource();
}
// Configure the data source and spec for the chunk.
DataSource dataSource = encryptedDataSource != null ? encryptedDataSource : upstreamDataSource;
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
// Compute start and end times, and the sequence number of the next chunk.
long startTimeUs;
if (live) {
if (previousTsChunk == null) {
startTimeUs = 0;
} else if (switchingVariantSpliced) {
startTimeUs = previousTsChunk.startTimeUs;
} else {
startTimeUs = previousTsChunk.endTimeUs;
}
} else /* Not live */ {
startTimeUs = segment.startTimeUs;
}
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1;
// Configure the extractor that will read the chunk.
TsExtractor extractor;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = new TsExtractor(startTimeUs, samplePool, switchingVariantSpliced);
} else {
extractor = previousTsChunk.extractor;
}
return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[variantIndex].index,
startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk);
}
/**
* Invoked when an error occurs loading a chunk.
*
* @param chunk The chunk whose load failed.
* @param e The failure.
* @return True if the error was handled by the source. False otherwise.
*/
public boolean onLoadError(HlsChunk chunk, IOException e) {
if ((chunk instanceof MediaPlaylistChunk) && (e instanceof InvalidResponseCodeException)) {
InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e;
int responseCode = responseCodeException.responseCode;
if (responseCode == 404 || responseCode == 410) {
MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk;
mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = true;
if (!allPlaylistsBlacklisted()) {
// We've handled the 404/410 by blacklisting the playlist.
Log.w(TAG, "Blacklisted playlist (" + responseCode + "): "
+ playlistChunk.dataSpec.uri);
return true;
} else {
// This was the last non-blacklisted playlist. Don't blacklist it.
Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): "
+ playlistChunk.dataSpec.uri);
mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = false;
return false;
}
}
}
return false;
}
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
int idealVariantIndex = getVariantIndexForBandwdith(
(int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION));
if (idealVariantIndex == variantIndex) {
// We're already using the ideal variant.
return variantIndex;
}
// We're not using the ideal variant for the available bandwidth, but only switch if the
// conditions are appropriate.
long bufferedPositionUs = previousTsChunk == null ? playbackPositionUs
: adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs;
if (mediaPlaylistBlacklistFlags[variantIndex]
|| (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
|| (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
// Switch variant.
return idealVariantIndex;
}
// Stick with the current variant for now.
return variantIndex;
}
private int getVariantIndexForBandwdith(int bandwidth) {
int lowestQualityEnabledVariant = 0;
for (int i = 0; i < enabledVariants.length; i++) {
if (!mediaPlaylistBlacklistFlags[i]) {
if (enabledVariants[i].bandwidth <= bandwidth) {
return i;
}
lowestQualityEnabledVariant = i;
}
}
return lowestQualityEnabledVariant;
}
private boolean shouldRerequestMediaPlaylist(int variantIndex) {
// Don't re-request media playlist more often than one-half of the target duration.
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
long timeSinceLastMediaPlaylistLoadMs =
SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex];
return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2;
}
private int getLiveStartChunkMediaSequence(int variantIndex) {
// For live start playback from the third chunk from the end.
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0;
return chunkIndex + mediaPlaylist.mediaSequence;
}
private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) {
Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri);
}
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null);
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv);
}
/* package */ void initEncryptedDataSource(Uri keyUri, String iv, byte[] secretKey) {
String trimmedIv;
if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
trimmedIv = iv.substring(2);
} else {
trimmedIv = iv;
}
byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray();
byte[] ivDataWithPadding = new byte[16];
int offset = ivData.length > 16 ? ivData.length - 16 : 0;
System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+ offset, ivData.length - offset);
encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource);
encryptionKeyUri = keyUri;
encryptedDataSourceIv = iv;
encryptedDataSourceSecretKey = secretKey;
}
private void clearEncryptedDataSource() {
encryptionKeyUri = null;
encryptedDataSource = null;
encryptedDataSourceIv = null;
encryptedDataSourceSecretKey = null;
}
/* package */ void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) {
lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime();
mediaPlaylists[variantIndex] = mediaPlaylist;
live |= mediaPlaylist.live;
durationUs = mediaPlaylist.durationUs;
}
private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) {
List<Variant> masterVariants = masterPlaylist.variants;
ArrayList<Variant> enabledVariants = new ArrayList<Variant>();
if (variantIndices != null) {
for (int i = 0; i < variantIndices.length; i++) {
enabledVariants.add(masterVariants.get(variantIndices[i]));
}
} else {
// If variantIndices is null then all variants are initially considered.
enabledVariants.addAll(masterVariants);
}
ArrayList<Variant> definiteVideoVariants = new ArrayList<Variant>();
ArrayList<Variant> definiteAudioOnlyVariants = new ArrayList<Variant>();
for (int i = 0; i < enabledVariants.size(); i++) {
Variant variant = enabledVariants.get(i);
if (variant.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) {
definiteVideoVariants.add(variant);
} else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) {
definiteAudioOnlyVariants.add(variant);
}
}
if (!definiteVideoVariants.isEmpty()) {
// We've identified some variants as definitely containing video. Assume variants within the
// master playlist are marked consistently, and hence that we have the full set. Filter out
// any other variants, which are likely to be audio only.
enabledVariants = definiteVideoVariants;
} else if (definiteAudioOnlyVariants.size() < enabledVariants.size()) {
// We've identified some variants, but not all, as being audio only. Filter them out to leave
// the remaining variants, which are likely to contain video.
enabledVariants.removeAll(definiteAudioOnlyVariants);
} else {
// Leave the enabled variants unchanged. They're likely either all video or all audio.
}
Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator());
Variant[] enabledVariantsArray = new Variant[enabledVariants.size()];
enabledVariants.toArray(enabledVariantsArray);
return enabledVariantsArray;
}
private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) {
String[] codecs = variant.codecs;
if (codecs == null) {
return false;
}
for (int i = 0; i < codecs.length; i++) {
if (codecs[i].startsWith(prefix)) {
return true;
}
}
return false;
}
private boolean allPlaylistsBlacklisted() {
for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) {
if (!mediaPlaylistBlacklistFlags[i]) {
return false;
}
}
return true;
}
private class MediaPlaylistChunk extends BitArrayChunk {
@SuppressWarnings("hiding")
/* package */ final int variantIndex;
private final Uri playlistBaseUri;
public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec,
Uri playlistBaseUri) {
super(dataSource, dataSpec, bitArray);
this.variantIndex = variantIndex;
this.playlistBaseUri = playlistBaseUri;
}
@Override
protected void consume(BitArray data) throws IOException {
HlsPlaylist playlist = playlistParser.parse(
new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null,
playlistBaseUri);
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
setMediaPlaylist(variantIndex, mediaPlaylist);
}
}
private class EncryptionKeyChunk extends BitArrayChunk {
private final String iv;
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) {
super(dataSource, dataSpec, bitArray);
this.iv = iv;
}
@Override
protected void consume(BitArray data) throws IOException {
byte[] secretKey = new byte[data.bytesLeft()];
data.readBytes(secretKey, 0, secretKey.length);
initEncryptedDataSource(dataSpec.uri, iv, secretKey);
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.hls;
import android.net.Uri;
import java.util.List;
/**
* Represents an HLS master playlist.
*/
public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Variant> variants;
public HlsMasterPlaylist(Uri baseUri, List<Variant> variants) {
super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = variants;
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.hls;
import com.google.android.exoplayer.C;
import android.net.Uri;
import java.util.List;
/**
* Represents an HLS media playlist.
*/
public final class HlsMediaPlaylist extends HlsPlaylist {
/**
* Media segment reference.
*/
public static final class Segment implements Comparable<Long> {
public final boolean discontinuity;
public final double durationSecs;
public final String url;
public final long startTimeUs;
public final String encryptionMethod;
public final String encryptionKeyUri;
public final String encryptionIV;
public final int byterangeOffset;
public final int byterangeLength;
public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs,
String encryptionMethod, String encryptionKeyUri, String encryptionIV,
int byterangeOffset, int byterangeLength) {
this.url = uri;
this.durationSecs = durationSecs;
this.discontinuity = discontinuity;
this.startTimeUs = startTimeUs;
this.encryptionMethod = encryptionMethod;
this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset;
this.byterangeLength = byterangeLength;
}
@Override
public int compareTo(Long startTimeUs) {
return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0);
}
}
public static final String ENCRYPTION_METHOD_NONE = "NONE";
public static final String ENCRYPTION_METHOD_AES_128 = "AES-128";
public final int mediaSequence;
public final int targetDurationSecs;
public final int version;
public final List<Segment> segments;
public final boolean live;
public final long durationUs;
public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version,
boolean live, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence;
this.targetDurationSecs = targetDurationSecs;
this.version = version;
this.live = live;
this.segments = segments;
if (!segments.isEmpty()) {
Segment last = segments.get(segments.size() - 1);
durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND);
} else {
durationUs = 0;
}
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.hls;
import com.google.android.exoplayer.ParserException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility methods for HLS manifest parsing.
*/
/* package */ class HlsParserUtil {
private HlsParserUtil() {}
public static String parseStringAttr(String line, Pattern pattern, String tag)
throws ParserException {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return null;
}
public static int parseIntAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, tag));
}
public static double parseDoubleAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Double.parseDouble(parseStringAttr(line, pattern, tag));
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.hls;
import android.net.Uri;
/**
* Represents an HLS playlist.
*/
public abstract class HlsPlaylist {
public final static int TYPE_MASTER = 0;
public final static int TYPE_MEDIA = 1;
public final Uri baseUri;
public final int type;
protected HlsPlaylist(Uri baseUri, int type) {
this.baseUri = baseUri;
this.type = type;
}
}

View File

@ -0,0 +1,282 @@
/*
* 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.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer.util.ManifestParser;
import android.net.Uri;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.regex.Pattern;
/**
* HLS playlists parsing logic.
*/
public final class HlsPlaylistParser implements ManifestParser<HlsPlaylist> {
private static final String VERSION_TAG = "#EXT-X-VERSION";
private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF";
private static final String BANDWIDTH_ATTR = "BANDWIDTH";
private static final String CODECS_ATTR = "CODECS";
private static final String RESOLUTION_ATTR = "RESOLUTION";
private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY";
private static final String MEDIA_DURATION_TAG = "#EXTINF";
private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE";
private static final String TARGET_DURATION_TAG = "#EXT-X-TARGETDURATION";
private static final String ENDLIST_TAG = "#EXT-X-ENDLIST";
private static final String KEY_TAG = "#EXT-X-KEY";
private static final String BYTERANGE_TAG = "#EXT-X-BYTERANGE";
private static final String METHOD_ATTR = "METHOD";
private static final String URI_ATTR = "URI";
private static final String IV_ATTR = "IV";
private static final Pattern BANDWIDTH_ATTR_REGEX =
Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b");
private static final Pattern CODECS_ATTR_REGEX =
Pattern.compile(CODECS_ATTR + "=\"(.+)\"");
private static final Pattern RESOLUTION_ATTR_REGEX =
Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)");
private static final Pattern MEDIA_DURATION_REGEX =
Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),");
private static final Pattern MEDIA_SEQUENCE_REGEX =
Pattern.compile(MEDIA_SEQUENCE_TAG + ":(\\d+)\\b");
private static final Pattern TARGET_DURATION_REGEX =
Pattern.compile(TARGET_DURATION_TAG + ":(\\d+)\\b");
private static final Pattern VERSION_REGEX =
Pattern.compile(VERSION_TAG + ":(\\d+)\\b");
private static final Pattern BYTERANGE_REGEX =
Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b");
private static final Pattern METHOD_ATTR_REGEX =
Pattern.compile(METHOD_ATTR + "=([^,.*]+)");
private static final Pattern URI_ATTR_REGEX =
Pattern.compile(URI_ATTR + "=\"(.+)\"");
private static final Pattern IV_ATTR_REGEX =
Pattern.compile(IV_ATTR + "=([^,.*]+)");
@Override
public HlsPlaylist parse(InputStream inputStream, String inputEncoding,
String contentId, Uri baseUri) throws IOException {
BufferedReader reader = new BufferedReader((inputEncoding == null)
? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding));
Queue<String> extraLines = new LinkedList<String>();
String line;
try {
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) {
// Do nothing.
} else if (line.startsWith(STREAM_INF_TAG)) {
extraLines.add(line);
return parseMasterPlaylist(new LineIterator(extraLines, reader), baseUri);
} else if (line.startsWith(TARGET_DURATION_TAG)
|| line.startsWith(MEDIA_SEQUENCE_TAG)
|| line.startsWith(MEDIA_DURATION_TAG)
|| line.startsWith(KEY_TAG)
|| line.startsWith(BYTERANGE_TAG)
|| line.equals(DISCONTINUITY_TAG)
|| line.equals(ENDLIST_TAG)) {
extraLines.add(line);
return parseMediaPlaylist(new LineIterator(extraLines, reader), baseUri);
} else if (line.startsWith(VERSION_TAG)) {
extraLines.add(line);
} else if (!line.startsWith("#")) {
throw new ParserException("Missing a tag before URL.");
}
}
} finally {
reader.close();
}
throw new ParserException("Failed to parse the playlist, could not identify any tags.");
}
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Uri baseUri)
throws IOException {
List<Variant> variants = new ArrayList<Variant>();
int bandwidth = 0;
String[] codecs = null;
int width = -1;
int height = -1;
int variantIndex = 0;
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(STREAM_INF_TAG)) {
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
if (codecsString != null) {
codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
} else {
codecs = null;
}
String resolutionString = HlsParserUtil.parseOptionalStringAttr(line,
RESOLUTION_ATTR_REGEX);
if (resolutionString != null) {
String[] widthAndHeight = resolutionString.split("x");
width = Integer.parseInt(widthAndHeight[0]);
height = Integer.parseInt(widthAndHeight[1]);
} else {
width = -1;
height = -1;
}
} else if (!line.startsWith("#")) {
variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
bandwidth = 0;
codecs = null;
width = -1;
height = -1;
}
}
return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants));
}
private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, Uri baseUri)
throws IOException {
int mediaSequence = 0;
int targetDurationSecs = 0;
int version = 1; // Default version == 1.
boolean live = true;
List<Segment> segments = new ArrayList<Segment>();
double segmentDurationSecs = 0.0;
boolean segmentDiscontinuity = false;
long segmentStartTimeUs = 0;
String segmentEncryptionMethod = null;
String segmentEncryptionKeyUri = null;
String segmentEncryptionIV = null;
int segmentByterangeOffset = 0;
int segmentByterangeLength = C.LENGTH_UNBOUNDED;
int segmentMediaSequence = 0;
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(TARGET_DURATION_TAG)) {
targetDurationSecs = HlsParserUtil.parseIntAttr(line, TARGET_DURATION_REGEX,
TARGET_DURATION_TAG);
} else if (line.startsWith(MEDIA_SEQUENCE_TAG)) {
mediaSequence = HlsParserUtil.parseIntAttr(line, MEDIA_SEQUENCE_REGEX, MEDIA_SEQUENCE_TAG);
segmentMediaSequence = mediaSequence;
} else if (line.startsWith(VERSION_TAG)) {
version = HlsParserUtil.parseIntAttr(line, VERSION_REGEX, VERSION_TAG);
} else if (line.startsWith(MEDIA_DURATION_TAG)) {
segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX,
MEDIA_DURATION_TAG);
} else if (line.startsWith(KEY_TAG)) {
segmentEncryptionMethod = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX,
METHOD_ATTR);
if (segmentEncryptionMethod.equals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE)) {
segmentEncryptionKeyUri = null;
segmentEncryptionIV = null;
} else {
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
URI_ATTR);
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
if (segmentEncryptionIV == null) {
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
}
}
} else if (line.startsWith(BYTERANGE_TAG)) {
String byteRange = HlsParserUtil.parseStringAttr(line, BYTERANGE_REGEX, BYTERANGE_TAG);
String[] splitByteRange = byteRange.split("@");
segmentByterangeLength = Integer.parseInt(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByterangeOffset = Integer.parseInt(splitByteRange[1]);
}
} else if (line.equals(DISCONTINUITY_TAG)) {
segmentDiscontinuity = true;
} else if (!line.startsWith("#")) {
segmentMediaSequence++;
if (segmentByterangeLength == C.LENGTH_UNBOUNDED) {
segmentByterangeOffset = 0;
}
segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity,
segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri,
segmentEncryptionIV, segmentByterangeOffset, segmentByterangeLength));
segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
segmentDiscontinuity = false;
segmentDurationSecs = 0.0;
if (segmentByterangeLength != C.LENGTH_UNBOUNDED) {
segmentByterangeOffset += segmentByterangeLength;
}
segmentByterangeLength = C.LENGTH_UNBOUNDED;
} else if (line.equals(ENDLIST_TAG)) {
live = false;
break;
}
}
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live,
Collections.unmodifiableList(segments));
}
private static class LineIterator {
private final BufferedReader reader;
private final Queue<String> extraLines;
private String next;
public LineIterator(Queue<String> extraLines, BufferedReader reader) {
this.extraLines = extraLines;
this.reader = reader;
}
public boolean hasNext() throws IOException {
if (next != null) {
return true;
}
if (!extraLines.isEmpty()) {
next = extraLines.poll();
return true;
}
while ((next = reader.readLine()) != null) {
next = next.trim();
if (!next.isEmpty()) {
return true;
}
}
return false;
}
public String next() throws IOException {
String result = null;
if (hasNext()) {
result = next;
next = null;
}
return result;
}
}
}

View File

@ -0,0 +1,447 @@
/*
* 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.hls;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import android.os.SystemClock;
import java.io.IOException;
import java.util.LinkedList;
/**
* A {@link SampleSource} for HLS streams.
*/
public class HlsSampleSource implements SampleSource, Loader.Callback {
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource;
private final LinkedList<TsExtractor> extractors;
private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount;
private int remainingReleaseCount;
private boolean prepared;
private int trackCount;
private int enabledTrackCount;
private boolean[] trackEnabledStates;
private boolean[] pendingDiscontinuities;
private TrackInfo[] trackInfos;
private MediaFormat[] downstreamMediaFormats;
private long downstreamPositionUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private TsChunk previousTsLoadable;
private HlsChunk currentLoadable;
private boolean loadingFinished;
private Loader loader;
private IOException currentLoadableException;
private boolean currentLoadableExceptionFatal;
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking,
int downstreamRendererCount) {
this(chunkSource, frameAccurateSeeking, downstreamRendererCount,
DEFAULT_MIN_LOADABLE_RETRY_COUNT);
}
public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking,
int downstreamRendererCount, int minLoadableRetryCount) {
this.chunkSource = chunkSource;
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<TsExtractor>();
}
@Override
public boolean prepare() throws IOException {
if (prepared) {
return true;
}
if (loader == null) {
loader = new Loader("Loader:HLS");
}
continueBufferingInternal();
if (!extractors.isEmpty()) {
TsExtractor extractor = extractors.getFirst();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
pendingDiscontinuities = new boolean[trackCount];
downstreamMediaFormats = new MediaFormat[trackCount];
trackInfos = new TrackInfo[trackCount];
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractor.getFormat(i);
trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
}
prepared = true;
}
}
if (!prepared) {
maybeThrowLoadableException();
}
return prepared;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return trackCount;
}
@Override
public TrackInfo getTrackInfo(int track) {
Assertions.checkState(prepared);
return trackInfos[track];
}
@Override
public void enable(int track, long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(!trackEnabledStates[track]);
enabledTrackCount++;
trackEnabledStates[track] = true;
downstreamMediaFormats[track] = null;
if (enabledTrackCount == 1) {
seekToUs(positionUs);
}
}
@Override
public void disable(int track) {
Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--;
trackEnabledStates[track] = false;
pendingDiscontinuities[track] = false;
if (enabledTrackCount == 0) {
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
}
}
}
@Override
public boolean continueBuffering(long playbackPositionUs) throws IOException {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
downstreamPositionUs = playbackPositionUs;
if (!extractors.isEmpty()) {
discardSamplesForDisabledTracks(extractors.getFirst(), downstreamPositionUs);
}
return continueBufferingInternal();
}
private boolean continueBufferingInternal() throws IOException {
maybeStartLoading();
if (isPendingReset() || extractors.isEmpty()) {
return false;
}
boolean haveSamples = prepared && haveSamplesForEnabledTracks(getCurrentExtractor());
if (!haveSamples) {
maybeThrowLoadableException();
}
return haveSamples;
}
@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
Assertions.checkState(prepared);
downstreamPositionUs = playbackPositionUs;
if (pendingDiscontinuities[track]) {
pendingDiscontinuities[track] = false;
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
maybeThrowLoadableException();
return NOTHING_READ;
}
TsExtractor extractor = getCurrentExtractor();
if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one.
extractor.configureSpliceTo(extractors.get(1));
}
int extractorIndex = 0;
while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) {
// We're finished reading from the extractor for this particular track, so advance to the
// next one for the current read.
extractor = extractors.get(++extractorIndex);
}
if (!extractor.isPrepared()) {
maybeThrowLoadableException();
return NOTHING_READ;
}
MediaFormat mediaFormat = extractor.getFormat(track);
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track], true)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
downstreamMediaFormats[track] = mediaFormat;
return FORMAT_READ;
}
if (extractor.getSample(track, sampleHolder)) {
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
return SAMPLE_READ;
}
if (loadingFinished) {
return END_OF_STREAM;
}
maybeThrowLoadableException();
return NOTHING_READ;
}
@Override
public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
lastSeekPositionUs = positionUs;
if (pendingResetPositionUs == positionUs || downstreamPositionUs == positionUs) {
downstreamPositionUs = positionUs;
return;
}
downstreamPositionUs = positionUs;
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
restartFrom(positionUs);
}
@Override
public long getBufferedPositionUs() {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
if (isPendingReset()) {
return pendingResetPositionUs;
} else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else {
long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp();
return largestSampleTimestamp == Long.MIN_VALUE ? downstreamPositionUs
: largestSampleTimestamp;
}
}
@Override
public void release() {
Assertions.checkState(remainingReleaseCount > 0);
if (--remainingReleaseCount == 0 && loader != null) {
loader.release();
loader = null;
}
}
@Override
public void onLoadCompleted(Loadable loadable) {
try {
currentLoadable.consume();
} catch (IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
currentLoadableExceptionFatal = true;
} finally {
if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk;
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
maybeStartLoading();
}
}
@Override
public void onLoadCanceled(Loadable loadable) {
if (enabledTrackCount > 0) {
restartFrom(pendingResetPositionUs);
} else {
clearState();
}
}
@Override
public void onLoadError(Loadable loadable, IOException e) {
if (chunkSource.onLoadError(currentLoadable, e)) {
// Error handled by source.
clearCurrentLoadable();
} else {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
}
maybeStartLoading();
}
/**
* Gets the current extractor from which samples should be read.
* <p>
* Calling this method discards extractors without any samples from the front of the queue. The
* last extractor is retained even if it doesn't have any samples.
* <p>
* This method must not be called unless {@link #extractors} is non-empty.
*
* @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/
private TsExtractor getCurrentExtractor() {
TsExtractor extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().release();
extractor = extractors.getFirst();
}
return extractor;
}
private void discardSamplesForDisabledTracks(TsExtractor extractor, long timeUs) {
if (!extractor.isPrepared()) {
return;
}
for (int i = 0; i < trackEnabledStates.length; i++) {
if (!trackEnabledStates[i]) {
extractor.discardUntil(i, timeUs);
}
}
}
private boolean haveSamplesForEnabledTracks(TsExtractor extractor) {
if (!extractor.isPrepared()) {
return false;
}
for (int i = 0; i < trackEnabledStates.length; i++) {
if (trackEnabledStates[i] && extractor.hasSamples(i)) {
return true;
}
}
return false;
}
private void maybeThrowLoadableException() throws IOException {
if (currentLoadableException != null && (currentLoadableExceptionFatal
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
throw currentLoadableException;
}
}
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
maybeStartLoading();
}
}
private void clearState() {
for (int i = 0; i < extractors.size(); i++) {
extractors.get(i).release();
}
extractors.clear();
clearCurrentLoadable();
previousTsLoadable = null;
}
private void clearCurrentLoadable() {
currentLoadable = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
currentLoadableExceptionFatal = false;
}
private void maybeStartLoading() {
if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) {
return;
}
boolean isBackedOff = currentLoadableException != null;
if (isBackedOff) {
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
currentLoadableException = null;
loader.startLoading(currentLoadable, this);
}
return;
}
HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
pendingResetPositionUs, downstreamPositionUs);
if (nextLoadable == null) {
return;
}
currentLoadable = nextLoadable;
if (isTsChunk(currentLoadable)) {
previousTsLoadable = (TsChunk) currentLoadable;
if (isPendingReset()) {
pendingResetPositionUs = NO_RESET_PENDING;
}
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) {
extractors.addLast(previousTsLoadable.extractor);
}
}
loader.startLoading(currentLoadable, this);
}
private boolean isTsChunk(HlsChunk chunk) {
return chunk instanceof TsChunk;
}
private boolean isPendingReset() {
return pendingResetPositionUs != NO_RESET_PENDING;
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
protected final int usToMs(long timeUs) {
return (int) (timeUs / 1000);
}
}

View File

@ -0,0 +1,132 @@
/*
* 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.hls;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import java.io.IOException;
/**
* A MPEG2TS chunk.
*/
public final class TsChunk extends HlsChunk {
private static final byte[] SCRATCH_SPACE = new byte[4096];
/**
* The index of the variant in the master playlist.
*/
public final int variantIndex;
/**
* The start time of the media contained by the chunk.
*/
public final long startTimeUs;
/**
* The end time of the media contained by the chunk.
*/
public final long endTimeUs;
/**
* The chunk index.
*/
public final int chunkIndex;
/**
* True if this is the last chunk in the media. False otherwise.
*/
public final boolean isLastChunk;
/**
* The extractor into which this chunk is being consumed.
*/
public final TsExtractor extractor;
private int loadPosition;
private volatile boolean loadFinished;
private volatile boolean loadCanceled;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param variantIndex The index of the variant in the master playlist.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec);
this.extractor = tsExtractor;
this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.chunkIndex = chunkIndex;
this.isLastChunk = isLastChunk;
}
@Override
public void consume() throws IOException {
// Do nothing.
}
@Override
public boolean isLoadFinished() {
return loadFinished;
}
// Loadable implementation
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
try {
dataSource.open(dataSpec);
int bytesRead = 0;
int bytesSkipped = 0;
// If we previously fed part of this chunk to the extractor, skip it this time.
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
// rather than loading the whole chunk again and then skipping data we previously loaded. To
// do this is straightforward for non-encrypted content, but more complicated for content
// encrypted with AES, for which we'll need to modify the way that decryption is performed.
while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) {
int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length);
bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength);
if (bytesRead != -1) {
bytesSkipped += bytesRead;
}
}
// Feed the remaining data into the extractor.
while (bytesRead != -1 && !loadCanceled) {
bytesRead = extractor.read(dataSource);
if (bytesRead != -1) {
loadPosition += bytesRead;
}
}
loadFinished = !loadCanceled;
} finally {
dataSource.close();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
/*
* 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.hls;
import java.util.Comparator;
/**
* Variant stream reference.
*/
public final class Variant {
/**
* Sorts {@link Variant} objects in order of decreasing bandwidth.
* <p>
* When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first.
*/
public static final class DecreasingBandwidthComparator implements Comparator<Variant> {
@Override
public int compare(Variant a, Variant b) {
int bandwidthDifference = b.bandwidth - a.bandwidth;
return bandwidthDifference != 0 ? bandwidthDifference : a.index - b.index;
}
}
public final int index;
public final int bandwidth;
public final String url;
public final String[] codecs;
public final int width;
public final int height;
public Variant(int index, String url, int bandwidth, String[] codecs, int width, int height) {
this.index = index;
this.bandwidth = bandwidth;
this.url = url;
this.codecs = codecs;
this.width = width;
this.height = height;
}
}

View File

@ -0,0 +1,159 @@
/*
* 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.metadata;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.MimeTypes;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Extracts individual TXXX text frames from raw ID3 data.
*/
public class Id3Parser implements MetadataParser<Map<String, Object>> {
@Override
public boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_ID3);
}
@Override
public Map<String, Object> parse(byte[] data, int size)
throws UnsupportedEncodingException, ParserException {
BitArray id3Buffer = new BitArray(data, size);
int id3Size = parseId3Header(id3Buffer);
Map<String, Object> metadata = new HashMap<String, Object>();
while (id3Size > 0) {
int frameId0 = id3Buffer.readUnsignedByte();
int frameId1 = id3Buffer.readUnsignedByte();
int frameId2 = id3Buffer.readUnsignedByte();
int frameId3 = id3Buffer.readUnsignedByte();
int frameSize = id3Buffer.readSynchSafeInt();
if (frameSize <= 1) {
break;
}
id3Buffer.skipBytes(2); // Skip frame flags.
// Check Frame ID == TXXX.
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
int encoding = id3Buffer.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1];
id3Buffer.readBytes(frame, 0, frameSize - 1);
int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String description = new String(frame, 0, firstZeroIndex, charset);
int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0);
int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex,
charset);
metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value));
} else {
String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize];
id3Buffer.readBytes(frame, 0, frameSize);
metadata.put(type, frame);
}
id3Size -= frameSize + 10 /* header size */;
}
return Collections.unmodifiableMap(metadata);
}
private static int indexOf(byte[] data, int fromIndex, byte key) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] == key) {
return i;
}
}
return data.length;
}
private static int indexOfNot(byte[] data, int fromIndex, byte key) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] != key) {
return i;
}
}
return data.length;
}
/**
* Parses ID3 header.
* @param id3Buffer A {@link BitArray} with raw ID3 data.
* @return The size of data that contains ID3 frames without header and footer.
* @throws ParserException If ID3 file identifier != "ID3".
*/
private static int parseId3Header(BitArray id3Buffer) throws ParserException {
int id1 = id3Buffer.readUnsignedByte();
int id2 = id3Buffer.readUnsignedByte();
int id3 = id3Buffer.readUnsignedByte();
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
throw new ParserException(String.format(
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
}
id3Buffer.skipBytes(2); // Skip version.
int flags = id3Buffer.readUnsignedByte();
int id3Size = id3Buffer.readSynchSafeInt();
// Check if extended header presents.
if ((flags & 0x2) != 0) {
int extendedHeaderSize = id3Buffer.readSynchSafeInt();
if (extendedHeaderSize > 4) {
id3Buffer.skipBytes(extendedHeaderSize - 4);
}
id3Size -= extendedHeaderSize;
}
// Check if footer presents.
if ((flags & 0x8) != 0) {
id3Size -= 10;
}
return id3Size;
}
/**
* Maps encoding byte from ID3v2 frame to a Charset.
* @param encodingByte The value of encoding byte from ID3v2 frame.
* @return Charset name.
*/
private static String getCharsetName(int encodingByte) {
switch (encodingByte) {
case 0:
return "ISO-8859-1";
case 1:
return "UTF-16";
case 2:
return "UTF-16BE";
case 3:
return "UTF-8";
default:
return "ISO-8859-1";
}
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.metadata;
import java.io.IOException;
/**
* Parses objects of type <T> from binary data.
*
* @param <T> The type of the metadata.
*/
public interface MetadataParser<T> {
/**
* Checks whether the parser supports a given mime type.
*
* @param mimeType A metadata mime type.
* @return Whether the mime type is supported.
*/
public boolean canParse(String mimeType);
/**
* Parses metadata objects of type <T> from the provided binary data.
*
* @param data The raw binary data from which to parse the metadata.
* @param size The size of the input data.
* @return @return A parsed metadata object of type <T>.
* @throws IOException If a problem occurred parsing the data.
*/
public T parse(byte[] data, int size) throws IOException;
}

View File

@ -0,0 +1,214 @@
/*
* 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.metadata;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.util.Assertions;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
/**
* A {@link TrackRenderer} for metadata embedded in a media stream.
*
* @param <T> The type of the metadata.
*/
public class MetadataTrackRenderer<T> extends TrackRenderer implements Callback {
/**
* An interface for components that process metadata.
*
* @param <T> The type of the metadata.
*/
public interface MetadataRenderer<T> {
/**
* Invoked each time there is a metadata associated with current playback time.
*
* @param metadata The metadata to process.
*/
void onMetadata(T metadata);
}
private static final int MSG_INVOKE_RENDERER = 0;
private final SampleSource source;
private final MetadataParser<T> metadataParser;
private final MetadataRenderer<T> metadataRenderer;
private final Handler metadataHandler;
private final MediaFormatHolder formatHolder;
private final SampleHolder sampleHolder;
private int trackIndex;
private long currentPositionUs;
private boolean inputStreamEnded;
private long pendingMetadataTimestamp;
private T pendingMetadata;
/**
* @param source A source from which samples containing metadata can be read.
* @param metadataParser A parser for parsing the metadata.
* @param metadataRenderer The metadata renderer to receive the parsed metadata.
* @param metadataRendererLooper The looper associated with the thread on which metadataRenderer
* should be invoked. If the renderer makes use of standard Android UI components, then this
* should normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
*/
public MetadataTrackRenderer(SampleSource source, MetadataParser<T> metadataParser,
MetadataRenderer<T> metadataRenderer, Looper metadataRendererLooper) {
this.source = Assertions.checkNotNull(source);
this.metadataParser = Assertions.checkNotNull(metadataParser);
this.metadataRenderer = Assertions.checkNotNull(metadataRenderer);
this.metadataHandler = metadataRendererLooper == null ? null
: new Handler(metadataRendererLooper, this);
formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@Override
protected int doPrepare() throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
for (int i = 0; i < source.getTrackCount(); i++) {
if (metadataParser.canParse(source.getTrackInfo(i).mimeType)) {
trackIndex = i;
return TrackRenderer.STATE_PREPARED;
}
}
return TrackRenderer.STATE_IGNORE;
}
@Override
protected void onEnabled(long positionUs, boolean joining) {
source.enable(trackIndex, positionUs);
seekToInternal(positionUs);
}
@Override
protected void seekTo(long positionUs) throws ExoPlaybackException {
source.seekToUs(positionUs);
seekToInternal(positionUs);
}
private void seekToInternal(long positionUs) {
currentPositionUs = positionUs;
pendingMetadata = null;
inputStreamEnded = false;
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
currentPositionUs = positionUs;
try {
source.continueBuffering(positionUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
if (!inputStreamEnded && pendingMetadata == null) {
try {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
pendingMetadataTimestamp = sampleHolder.timeUs;
pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size);
sampleHolder.data.clear();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
if (pendingMetadata != null && pendingMetadataTimestamp <= currentPositionUs) {
invokeRenderer(pendingMetadata);
pendingMetadata = null;
}
}
@Override
protected void onDisabled() {
pendingMetadata = null;
source.disable(trackIndex);
}
@Override
protected long getDurationUs() {
return source.getTrackInfo(trackIndex).durationUs;
}
@Override
protected long getCurrentPositionUs() {
return currentPositionUs;
}
@Override
protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK_US;
}
@Override
protected boolean isEnded() {
return inputStreamEnded;
}
@Override
protected boolean isReady() {
return true;
}
private void invokeRenderer(T metadata) {
if (metadataHandler != null) {
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else {
invokeRendererInternal(metadata);
}
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVOKE_RENDERER:
invokeRendererInternal((T) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternal(T metadata) {
metadataRenderer.onMetadata(metadata);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.metadata;
/**
* A metadata that contains parsed ID3 TXXX (User defined text information) frame data associated
* with time indices.
*/
public class TxxxMetadata {
public static final String TYPE = "TXXX";
public final String description;
public final String value;
public TxxxMetadata(String description, String value) {
this.description = description;
this.value = value;
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.text;
/**
* An interface for components that render text.
*/
public interface TextRenderer {
/**
* Invoked each time there is a change in the text to be rendered.
*
* @param text The text to render, or null if no text is to be rendered.
*/
void onText(String text);
}

View File

@ -38,20 +38,6 @@ import java.io.IOException;
@TargetApi(16)
public class TextTrackRenderer extends TrackRenderer implements Callback {
/**
* An interface for components that render text.
*/
public interface TextRenderer {
/**
* Invoked each time there is a change in the text to be rendered.
*
* @param text The text to render, or null if no text is to be rendered.
*/
void onText(String text);
}
private static final int MSG_UPDATE_OVERLAY = 0;
private final Handler textRendererHandler;

View File

@ -0,0 +1,62 @@
/*
* 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.text.eia608;
/**
* A Closed Caption that contains textual data associated with time indices.
*/
public final class ClosedCaption implements Comparable<ClosedCaption> {
/**
* Identifies closed captions with control characters.
*/
public static final int TYPE_CTRL = 0;
/**
* Identifies closed captions with textual information.
*/
public static final int TYPE_TEXT = 1;
/**
* The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field
* has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control
* characters (C1, C2).
*/
public final int type;
/**
* Contains text or two control characters.
*/
public final String text;
/**
* Timestamp associated with the closed caption.
*/
public final long timeUs;
public ClosedCaption(int type, String text, long timeUs) {
this.type = type;
this.text = text;
this.timeUs = timeUs;
}
@Override
public int compareTo(ClosedCaption another) {
long delta = this.timeUs - another.timeUs;
if (delta == 0) {
return 0;
}
return delta > 0 ? 1 : -1;
}
}

View File

@ -0,0 +1,215 @@
/*
* 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.text.eia608;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.MimeTypes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608")
* Closed Captions from the SEI data block from H.264.
*/
public class Eia608Parser {
private static final int PAYLOAD_TYPE_CC = 4;
private static final int COUNTRY_CODE = 0xB5;
private static final int PROVIDER_CODE = 0x31;
private static final int USER_ID = 0x47413934; // "GA94"
private static final int USER_DATA_TYPE_CODE = 0x3;
// Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
private static final int[] BASIC_CHARACTER_SET = new int[] {
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & '
0x28, 0x29, // ( )
0xE1, // 2A: 225 'á' "Latin small letter A with acute"
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . /
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7
0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ?
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W
0x58, 0x59, 0x5A, 0x5B, // X Y Z [
0xE9, // 5C: 233 'é' "Latin small letter E with acute"
0x5D, // ]
0xED, // 5E: 237 'í' "Latin small letter I with acute"
0xF3, // 5F: 243 'ó' "Latin small letter O with acute"
0xFA, // 60: 250 'ú' "Latin small letter U with acute"
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g
0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w
0x78, 0x79, 0x7A, // x y z
0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
0xF7, // 7C: 247 '÷' "Division sign"
0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde"
0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block)
};
// Special North American 608 CC char set.
private static final int[] SPECIAL_CHARACTER_SET = new int[] {
0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
0xB0, // 31: 176 '°' "Degree Sign"
0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
0xBF, // 33: 191 '¿' "Inverted Question Mark"
0x2122, // 34: "Trade Mark Sign" (tm superscript)
0xA2, // 35: 162 '¢' "Cent Sign"
0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling
0x266A, // 37: "Eighth Note" - music note
0xE0, // 38: 224 'à' "Latin small letter A with grave"
0x20, // 39: TRANSPARENT SPACE - for now use ordinary space
0xE8, // 3A: 232 'è' "Latin small letter E with grave"
0xE2, // 3B: 226 'â' "Latin small letter A with circumflex"
0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex"
0xEE, // 3D: 238 'î' "Latin small letter I with circumflex"
0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex"
0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
};
public boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_EIA608);
}
public List<ClosedCaption> parse(byte[] data, int size, long timeUs) throws IOException {
if (size <= 0) {
return null;
}
BitArray seiBuffer = new BitArray(data, size);
seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit
int ccCount = seiBuffer.readBits(5);
seiBuffer.skipBytes(1);
List<ClosedCaption> captions = new ArrayList<ClosedCaption>();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < ccCount; i++) {
seiBuffer.skipBits(5); // one_bit + reserved
boolean ccValid = seiBuffer.readBit();
if (!ccValid) {
seiBuffer.skipBits(18);
continue;
}
int ccType = seiBuffer.readBits(2);
if (ccType != 0) {
seiBuffer.skipBits(16);
continue;
}
seiBuffer.skipBits(1);
byte ccData1 = (byte) seiBuffer.readBits(7);
seiBuffer.skipBits(1);
byte ccData2 = (byte) seiBuffer.readBits(7);
// Ignore empty captions.
if (ccData1 == 0 && ccData2 == 0) {
continue;
}
// Special North American character set.
if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) {
stringBuilder.append(getSpecialChar(ccData2));
continue;
}
// Control character.
if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(),
timeUs));
stringBuilder.setLength(0);
}
captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL,
new String(new char[] {(char) ccData1, (char) ccData2}), timeUs));
continue;
}
// Basic North American character set.
stringBuilder.append(getChar(ccData1));
if (ccData2 != 0) {
stringBuilder.append(getChar(ccData2));
}
}
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), timeUs));
}
return Collections.unmodifiableList(captions);
}
private static char getChar(byte ccData) {
int index = (ccData & 0x7F) - 0x20;
return (char) BASIC_CHARACTER_SET[index];
}
private static char getSpecialChar(byte ccData) {
int index = ccData & 0xF;
return (char) SPECIAL_CHARACTER_SET[index];
}
/**
* Parses the beginning of SEI data and returns the size of underlying contains closed captions
* data following the header. Returns 0 if the SEI doesn't contain any closed captions data.
*
* @param seiBuffer The buffer to read from.
* @return The size of closed captions data.
*/
public static int parseHeader(BitArray seiBuffer) {
int b = 0;
int payloadType = 0;
do {
b = seiBuffer.readUnsignedByte();
payloadType += b;
} while (b == 0xFF);
if (payloadType != PAYLOAD_TYPE_CC) {
return 0;
}
int payloadSize = 0;
do {
b = seiBuffer.readUnsignedByte();
payloadSize += b;
} while (b == 0xFF);
if (payloadSize <= 0) {
return 0;
}
int countryCode = seiBuffer.readUnsignedByte();
if (countryCode != COUNTRY_CODE) {
return 0;
}
int providerCode = seiBuffer.readBits(16);
if (providerCode != PROVIDER_CODE) {
return 0;
}
int userIdentifier = seiBuffer.readBits(32);
if (userIdentifier != USER_ID) {
return 0;
}
int userDataTypeCode = seiBuffer.readUnsignedByte();
if (userDataTypeCode != USER_DATA_TYPE_CODE) {
return 0;
}
return payloadSize;
}
}

View File

@ -0,0 +1,260 @@
/*
* 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.text.eia608;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.util.Assertions;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* A {@link TrackRenderer} for EIA-608 closed captions in a media stream.
*/
public class Eia608TrackRenderer extends TrackRenderer implements Callback {
private static final int MSG_INVOKE_RENDERER = 0;
// The Number of closed captions text line to keep in memory.
private static final int ALLOWED_CAPTIONS_TEXT_LINES_COUNT = 4;
private final SampleSource source;
private final Eia608Parser eia608Parser;
private final TextRenderer textRenderer;
private final Handler metadataHandler;
private final MediaFormatHolder formatHolder;
private final SampleHolder sampleHolder;
private final StringBuilder closedCaptionStringBuilder;
//Currently displayed captions.
private final List<ClosedCaption> currentCaptions;
private final Queue<Integer> newLineIndexes;
private int trackIndex;
private long currentPositionUs;
private boolean inputStreamEnded;
private long pendingCaptionsTimestamp;
private List<ClosedCaption> pendingCaptions;
/**
* @param source A source from which samples containing EIA-608 closed captions can be read.
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
*/
public Eia608TrackRenderer(SampleSource source, TextRenderer textRenderer,
Looper textRendererLooper) {
this.source = Assertions.checkNotNull(source);
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.metadataHandler = textRendererLooper == null ? null
: new Handler(textRendererLooper, this);
eia608Parser = new Eia608Parser();
formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
closedCaptionStringBuilder = new StringBuilder();
currentCaptions = new LinkedList<ClosedCaption>();
newLineIndexes = new LinkedList<Integer>();
}
@Override
protected int doPrepare() throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
for (int i = 0; i < source.getTrackCount(); i++) {
if (eia608Parser.canParse(source.getTrackInfo(i).mimeType)) {
trackIndex = i;
return TrackRenderer.STATE_PREPARED;
}
}
return TrackRenderer.STATE_IGNORE;
}
@Override
protected void onEnabled(long positionUs, boolean joining) {
source.enable(trackIndex, positionUs);
seekToInternal(positionUs);
}
@Override
protected void seekTo(long positionUs) throws ExoPlaybackException {
source.seekToUs(positionUs);
seekToInternal(positionUs);
}
private void seekToInternal(long positionUs) {
currentPositionUs = positionUs;
pendingCaptions = null;
inputStreamEnded = false;
// Clear displayed captions.
currentCaptions.clear();
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
currentPositionUs = positionUs;
try {
source.continueBuffering(positionUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
if (!inputStreamEnded && pendingCaptions == null) {
try {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
pendingCaptionsTimestamp = sampleHolder.timeUs;
pendingCaptions = eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size,
sampleHolder.timeUs);
sampleHolder.data.clear();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
if (pendingCaptions != null && pendingCaptionsTimestamp <= currentPositionUs) {
invokeRenderer(pendingCaptions);
pendingCaptions = null;
}
}
@Override
protected void onDisabled() {
pendingCaptions = null;
source.disable(trackIndex);
}
@Override
protected long getDurationUs() {
return source.getTrackInfo(trackIndex).durationUs;
}
@Override
protected long getCurrentPositionUs() {
return currentPositionUs;
}
@Override
protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK_US;
}
@Override
protected boolean isEnded() {
return inputStreamEnded;
}
@Override
protected boolean isReady() {
return true;
}
private void invokeRenderer(List<ClosedCaption> metadata) {
if (metadataHandler != null) {
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else {
invokeRendererInternal(metadata);
}
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVOKE_RENDERER:
invokeRendererInternal((List<ClosedCaption>) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternal(List<ClosedCaption> metadata) {
currentCaptions.addAll(metadata);
// Sort captions by the timestamp.
Collections.sort(currentCaptions);
closedCaptionStringBuilder.setLength(0);
// After processing keep only captions after cutIndex.
int cutIndex = 0;
newLineIndexes.clear();
for (int i = 0; i < currentCaptions.size(); i++) {
ClosedCaption caption = currentCaptions.get(i);
if (caption.type == ClosedCaption.TYPE_CTRL) {
int cc2 = caption.text.codePointAt(1);
switch (cc2) {
case 0x2C: // Erase Displayed Memory.
closedCaptionStringBuilder.setLength(0);
cutIndex = i;
newLineIndexes.clear();
break;
case 0x25: // Roll-Up.
case 0x26:
case 0x27:
default:
if (cc2 >= 0x20 && cc2 < 0x40) {
break;
}
if (closedCaptionStringBuilder.length() > 0
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1)
!= '\n') {
closedCaptionStringBuilder.append('\n');
newLineIndexes.add(i);
if (newLineIndexes.size() >= ALLOWED_CAPTIONS_TEXT_LINES_COUNT) {
cutIndex = newLineIndexes.poll();
}
}
break;
}
} else {
closedCaptionStringBuilder.append(caption.text);
}
}
if (cutIndex > 0 && cutIndex < currentCaptions.size() - 1) {
for (int i = 0; i <= cutIndex; i++) {
currentCaptions.remove(0);
}
}
textRenderer.onText(closedCaptionStringBuilder.toString());
}
}

View File

@ -0,0 +1,97 @@
/*
* 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.upstream;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* A {@link DataSource} that decrypts the data read from an upstream source, encrypted with AES-128
* with a 128-bit key and PKCS7 padding.
*
*/
public class Aes128DataSource implements DataSource {
private final DataSource upstream;
private final byte[] secretKey;
private final byte[] iv;
private CipherInputStream cipherInputStream;
public Aes128DataSource(byte[] secretKey, byte[] iv, DataSource upstream) {
this.upstream = upstream;
this.secretKey = secretKey;
this.iv = iv;
}
@Override
public long open(DataSpec dataSpec) throws IOException {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (NoSuchPaddingException e) {
throw new RuntimeException(e);
}
Key cipherKey = new SecretKeySpec(secretKey, "AES");
AlgorithmParameterSpec cipherIV = new IvParameterSpec(iv);
try {
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
} catch (InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
cipherInputStream = new CipherInputStream(
new DataSourceInputStream(upstream, dataSpec), cipher);
return C.LENGTH_UNBOUNDED;
}
@Override
public void close() throws IOException {
cipherInputStream = null;
upstream.close();
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
Assertions.checkState(cipherInputStream != null);
int bytesRead = cipherInputStream.read(buffer, offset, readLength);
if (bytesRead < 0) {
return -1;
}
return bytesRead;
}
}

View File

@ -0,0 +1,361 @@
/*
* 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.util;
import com.google.android.exoplayer.upstream.DataSource;
import java.io.IOException;
/**
* Wraps a byte array, providing methods that allow it to be read as a bitstream.
*/
public final class BitArray {
private byte[] data;
// The length of the valid data.
private int limit;
// The offset within the data, stored as the current byte offset, and the bit offset within that
// byte (from 0 to 7).
private int byteOffset;
private int bitOffset;
public BitArray() {
}
public BitArray(byte[] data, int limit) {
this.data = data;
this.limit = limit;
}
/**
* Clears all data, setting the offset and limit to zero.
*/
public void reset() {
byteOffset = 0;
bitOffset = 0;
limit = 0;
}
/**
* Resets to wrap the specified data, setting the offset to zero.
*
* @param data The data to wrap.
* @param limit The limit to set.
*/
public void reset(byte[] data, int limit) {
this.data = data;
this.limit = limit;
byteOffset = 0;
bitOffset = 0;
}
/**
* Gets the backing byte array.
*
* @return The backing byte array.
*/
public byte[] getData() {
return data;
}
/**
* Gets the current byte offset.
*
* @return The current byte offset.
*/
public int getByteOffset() {
return byteOffset;
}
/**
* Sets the current byte offset.
*
* @param byteOffset The byte offset to set.
*/
public void setByteOffset(int byteOffset) {
this.byteOffset = byteOffset;
}
/**
* Appends data from a {@link DataSource}.
*
* @param dataSource The {@link DataSource} from which to read.
* @param length The maximum number of bytes to read and append.
* @return The number of bytes that were read and appended, or -1 if no more data is available.
* @throws IOException If an error occurs reading from the source.
*/
public int append(DataSource dataSource, int length) throws IOException {
expand(length);
int bytesRead = dataSource.read(data, limit, length);
if (bytesRead == -1) {
return -1;
}
limit += bytesRead;
return bytesRead;
}
/**
* Appends data from another {@link BitArray}.
*
* @param bitsArray The {@link BitArray} whose data should be appended.
* @param length The number of bytes to read and append.
*/
public void append(BitArray bitsArray, int length) {
expand(length);
bitsArray.readBytes(data, limit, length);
limit += length;
}
private void expand(int length) {
if (data == null) {
data = new byte[length];
return;
}
if (data.length - limit < length) {
byte[] newBuffer = new byte[limit + length];
System.arraycopy(data, 0, newBuffer, 0, limit);
data = newBuffer;
}
}
/**
* Clears data that has already been read, moving the remaining data to the start of the buffer.
*/
public void clearReadData() {
System.arraycopy(data, byteOffset, data, 0, limit - byteOffset);
limit -= byteOffset;
byteOffset = 0;
}
/**
* Reads a single unsigned byte.
*
* @return The value of the parsed byte.
*/
public int readUnsignedByte() {
int value;
if (bitOffset != 0) {
value = ((data[byteOffset] & 0xFF) << bitOffset)
| ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset));
} else {
value = data[byteOffset];
}
byteOffset++;
return value & 0xFF;
}
/**
* Reads a single bit.
*
* @return True if the bit is set. False otherwise.
*/
public boolean readBit() {
return readBits(1) == 1;
}
/**
* Reads up to 32 bits.
*
* @param n The number of bits to read.
* @return An integer whose bottom n bits hold the read data.
*/
public int readBits(int n) {
return (int) readBitsLong(n);
}
/**
* Reads up to 64 bits.
*
* @param n The number of bits to read.
* @return A long whose bottom n bits hold the read data.
*/
public long readBitsLong(int n) {
if (n == 0) {
return 0;
}
long retval = 0;
// While n >= 8, read whole bytes.
while (n >= 8) {
n -= 8;
retval |= (readUnsignedByte() << n);
}
if (n > 0) {
int nextBit = bitOffset + n;
byte writeMask = (byte) (0xFF >> (8 - n));
if (nextBit > 8) {
// Combine bits from current byte and next byte.
retval |= (((getUnsignedByte(byteOffset) << (nextBit - 8)
| (getUnsignedByte(byteOffset + 1) >> (16 - nextBit))) & writeMask));
byteOffset++;
} else {
// Bits to be read only within current byte.
retval |= ((getUnsignedByte(byteOffset) >> (8 - nextBit)) & writeMask);
if (nextBit == 8) {
byteOffset++;
}
}
bitOffset = nextBit % 8;
}
return retval;
}
private int getUnsignedByte(int offset) {
return data[offset] & 0xFF;
}
/**
* Skips bits and moves current reading position forward.
*
* @param n The number of bits to skip.
*/
public void skipBits(int n) {
byteOffset += (n / 8);
bitOffset += (n % 8);
if (bitOffset > 7) {
byteOffset++;
bitOffset -= 8;
}
}
/**
* Skips bytes and moves current reading position forward.
*
* @param n The number of bytes to skip.
*/
public void skipBytes(int n) {
byteOffset += n;
}
/**
* Reads multiple bytes and copies them into provided byte array.
* <p>
* The read position must be at a whole byte boundary for this method to be called.
*
* @param out The byte array to copy read data.
* @param offset The offset in the out byte array.
* @param length The length of the data to read
* @throws IllegalStateException If the method is called with the read position not at a whole
* byte boundary.
*/
public void readBytes(byte[] out, int offset, int length) {
Assertions.checkState(bitOffset == 0);
System.arraycopy(data, byteOffset, out, offset, length);
byteOffset += length;
}
/**
* @return The number of whole bytes that are available to read.
*/
public int bytesLeft() {
return limit - byteOffset;
}
/**
* @return Whether or not there is any data available.
*/
public boolean isEmpty() {
return limit == 0;
}
/**
* Reads an unsigned Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readUnsignedExpGolombCodedInt() {
return readExpGolombCodeNum();
}
/**
* Reads an signed Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readSignedExpGolombCodedInt() {
int codeNum = readExpGolombCodeNum();
return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
}
private int readExpGolombCodeNum() {
int leadingZeros = 0;
while (!readBit()) {
leadingZeros++;
}
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
}
/**
* Reads a Synchsafe integer.
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
* A 32 bit synchsafe integer can store 28 bits of information.
*
* @return The value of the parsed Synchsafe integer.
*/
public int readSynchSafeInt() {
int b1 = readUnsignedByte();
int b2 = readUnsignedByte();
int b3 = readUnsignedByte();
int b4 = readUnsignedByte();
return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
}
// TODO: Find a better place for this method.
/**
* Finds the next Adts sync word.
*
* @return The offset from the current position to the start of the next Adts sync word. If an
* Adts sync word is not found, then the offset to the end of the data is returned.
*/
public int findNextAdtsSyncWord() {
for (int i = byteOffset; i < limit - 1; i++) {
int syncBits = (getUnsignedByte(i) << 8) | getUnsignedByte(i + 1);
if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) {
return i - byteOffset;
}
}
return limit - byteOffset;
}
//TODO: Find a better place for this method.
/**
* Finds the next NAL unit.
*
* @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit.
* @param offset The additional offset in the data to start the search from.
* @return The offset from the current position to the start of the NAL unit. If a NAL unit is
* not found, then the offset to the end of the data is returned.
*/
public int findNextNalUnit(int nalUnitType, int offset) {
for (int i = byteOffset + offset; i < limit - 3; i++) {
// Check for NAL unit start code prefix == 0x000001.
if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1)
&& (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) {
return i - byteOffset;
}
}
return limit - byteOffset;
}
}

View File

@ -40,6 +40,8 @@ public class MimeTypes {
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
private MimeTypes() {}