commit
27c0e7d776
@ -50,6 +50,7 @@ public class DemoUtil {
|
|||||||
public static final int TYPE_DASH = 0;
|
public static final int TYPE_DASH = 0;
|
||||||
public static final int TYPE_SS = 1;
|
public static final int TYPE_SS = 1;
|
||||||
public static final int TYPE_OTHER = 2;
|
public static final int TYPE_OTHER = 2;
|
||||||
|
public static final int TYPE_HLS = 3;
|
||||||
|
|
||||||
public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false;
|
public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false;
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ public class SampleChooserActivity extends Activity {
|
|||||||
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
|
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
|
||||||
sampleAdapter.add(new Header("Misc"));
|
sampleAdapter.add(new Header("Misc"));
|
||||||
sampleAdapter.addAll((Object[]) Samples.MISC);
|
sampleAdapter.addAll((Object[]) Samples.MISC);
|
||||||
|
sampleAdapter.add(new Header("HLS"));
|
||||||
|
sampleAdapter.addAll((Object[]) Samples.HLS);
|
||||||
if (DemoUtil.EXPOSE_EXPERIMENTAL_FEATURES) {
|
if (DemoUtil.EXPOSE_EXPERIMENTAL_FEATURES) {
|
||||||
sampleAdapter.add(new Header("YouTube WebM DASH (Experimental)"));
|
sampleAdapter.add(new Header("YouTube WebM DASH (Experimental)"));
|
||||||
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM);
|
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM);
|
||||||
|
@ -52,6 +52,9 @@ package com.google.android.exoplayer.demo;
|
|||||||
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
|
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
|
||||||
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
|
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
|
||||||
DemoUtil.TYPE_SS, false),
|
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",
|
new Sample("Dizzy (Misc)", "uid:misc:dizzy",
|
||||||
"http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false),
|
"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),
|
+ "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[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
||||||
DemoUtil.TYPE_OTHER, true),
|
DemoUtil.TYPE_OTHER, true),
|
||||||
|
@ -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.DefaultRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.full.player.DemoPlayer;
|
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.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.SmoothStreamingRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.full.player.UnsupportedDrmException;
|
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.CaptionStyleCompat;
|
||||||
import com.google.android.exoplayer.text.SubtitleView;
|
import com.google.android.exoplayer.text.SubtitleView;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
@ -40,6 +42,7 @@ import android.graphics.Point;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@ -57,11 +60,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity that plays media using {@link DemoPlayer}.
|
* An activity that plays media using {@link DemoPlayer}.
|
||||||
*/
|
*/
|
||||||
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
|
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 float CAPTION_LINE_HEIGHT_RATIO = 0.0533f;
|
||||||
private static final int MENU_GROUP_TRACKS = 1;
|
private static final int MENU_GROUP_TRACKS = 1;
|
||||||
@ -199,6 +207,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
case DemoUtil.TYPE_DASH:
|
case DemoUtil.TYPE_DASH:
|
||||||
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
|
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
|
||||||
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
|
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
|
||||||
|
case DemoUtil.TYPE_HLS:
|
||||||
|
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId);
|
||||||
default:
|
default:
|
||||||
return new DefaultRendererBuilder(this, contentUri, debugTextView);
|
return new DefaultRendererBuilder(this, contentUri, debugTextView);
|
||||||
}
|
}
|
||||||
@ -209,6 +219,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
player = new DemoPlayer(getRendererBuilder());
|
player = new DemoPlayer(getRendererBuilder());
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
player.setTextListener(this);
|
player.setTextListener(this);
|
||||||
|
player.setMetadataListener(this);
|
||||||
player.seekTo(playerPosition);
|
player.seekTo(playerPosition);
|
||||||
playerNeedsPrepare = true;
|
playerNeedsPrepare = true;
|
||||||
mediaController.setMediaPlayer(player.getPlayerControl());
|
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
|
// SurfaceHolder.Callback implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -27,7 +27,8 @@ import com.google.android.exoplayer.audio.AudioTrack;
|
|||||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||||
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
||||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
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.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.util.PlayerControl;
|
import com.google.android.exoplayer.util.PlayerControl;
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ import android.os.Looper;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,7 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
||||||
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
|
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
|
||||||
MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener,
|
MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener,
|
||||||
TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener {
|
TextRenderer, StreamingDrmSessionManager.EventListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds renderers for the player.
|
* Builds renderers for the player.
|
||||||
@ -136,6 +138,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
void onText(String text);
|
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.
|
// Constants pulled into this class for convenience.
|
||||||
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
|
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
|
||||||
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
|
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 DISABLED_TRACK = -1;
|
||||||
public static final int PRIMARY_TRACK = 0;
|
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_VIDEO = 0;
|
||||||
public static final int TYPE_AUDIO = 1;
|
public static final int TYPE_AUDIO = 1;
|
||||||
public static final int TYPE_TEXT = 2;
|
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_IDLE = 1;
|
||||||
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
|
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 int[] selectedTracks;
|
||||||
|
|
||||||
private TextListener textListener;
|
private TextListener textListener;
|
||||||
|
private Id3MetadataListener id3MetadataListener;
|
||||||
private InternalErrorListener internalErrorListener;
|
private InternalErrorListener internalErrorListener;
|
||||||
private InfoListener infoListener;
|
private InfoListener infoListener;
|
||||||
|
|
||||||
@ -216,6 +227,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
textListener = listener;
|
textListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMetadataListener(Id3MetadataListener listener) {
|
||||||
|
id3MetadataListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
public void setSurface(Surface surface) {
|
public void setSurface(Surface surface) {
|
||||||
this.surface = surface;
|
this.surface = surface;
|
||||||
pushSurfaceAndVideoTrack(false);
|
pushSurfaceAndVideoTrack(false);
|
||||||
@ -465,9 +480,19 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onText(String text) {
|
public void onText(String text) {
|
||||||
if (textListener != null) {
|
processText(text);
|
||||||
textListener.onText(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
|
@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 class InternalRendererBuilderCallback implements RendererBuilderCallback {
|
||||||
|
|
||||||
private boolean canceled;
|
private boolean canceled;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -166,6 +166,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
|
|||||||
contentId);
|
contentId);
|
||||||
case DemoUtil.TYPE_DASH:
|
case DemoUtil.TYPE_DASH:
|
||||||
return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
|
return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
|
||||||
|
case DemoUtil.TYPE_HLS:
|
||||||
|
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId);
|
||||||
default:
|
default:
|
||||||
return new DefaultRendererBuilder(this, contentUri);
|
return new DefaultRendererBuilder(this, contentUri);
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,14 @@ public class MediaFormat {
|
|||||||
sampleRate, bitrate, initializationData);
|
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() {
|
public static MediaFormat createTtmlFormat() {
|
||||||
return createFormatForMimeType(MimeTypes.APPLICATION_TTML);
|
return createFormatForMimeType(MimeTypes.APPLICATION_TTML);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
}
|
@ -38,20 +38,6 @@ import java.io.IOException;
|
|||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
public class TextTrackRenderer extends TrackRenderer implements Callback {
|
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 static final int MSG_UPDATE_OVERLAY = 0;
|
||||||
|
|
||||||
private final Handler textRendererHandler;
|
private final Handler textRendererHandler;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -40,6 +40,8 @@ public class MimeTypes {
|
|||||||
|
|
||||||
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
|
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";
|
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
||||||
|
|
||||||
private MimeTypes() {}
|
private MimeTypes() {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user