Commit 34372deb by ojw28

Merge pull request #199 from google/dev

Release 1.1.0
parents 280ccb16 f15e3973
Showing with 2263 additions and 746 deletions
# How to contribute #
# How to Contribute #
We'd love to hear your feedback. Please open new issues describing any bugs,
feature requests or suggestions that you have.
......
......@@ -4,7 +4,7 @@
ExoPlayer is an application level media player for Android. It provides an
alternative to Android’s MediaPlayer API for playing audio and video both
locally and over the internet. ExoPlayer supports features not currently
locally and over the Internet. ExoPlayer supports features not currently
supported by Android’s MediaPlayer API (as of KitKat), including DASH and
SmoothStreaming adaptive playbacks, persistent caching and custom renderers.
Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and
......
......@@ -19,7 +19,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
classpath 'com.android.tools.build:gradle:1.0.0-rc1'
}
}
......
......@@ -11,19 +11,19 @@
// 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.
apply plugin: 'android'
apply plugin: 'com.android.application'
android {
compileSdkVersion 19
buildToolsVersion "19.1"
compileSdkVersion 21
buildToolsVersion "21.1.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 19
targetSdkVersion 21
}
buildTypes {
release {
runProguard false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
......
......@@ -4,7 +4,7 @@
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
......@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.demo"
android:versionCode="1013"
android:versionName="1.0.13"
android:versionCode="1100"
android:versionName="1.1.00"
android:theme="@style/RootTheme">
<uses-permission android:name="android.permission.INTERNET"/>
......@@ -25,11 +25,12 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="19"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21"/>
<application
android:label="@string/application_name"
android:allowBackup="true">
android:largeHeap="true"
android:allowBackup="false">
<activity android:name="com.google.android.exoplayer.demo.SampleChooserActivity"
android:configChanges="keyboardHidden"
......
......@@ -28,6 +28,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
......@@ -44,12 +47,19 @@ public class DemoUtil {
public static final String CONTENT_TYPE_EXTRA = "content_type";
public static final String CONTENT_ID_EXTRA = "content_id";
public static final int TYPE_DASH_VOD = 0;
public static final int TYPE_SS_VOD = 1;
public static final int TYPE_DASH = 0;
public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2;
public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false;
private static final CookieManager defaultCookieManager;
static {
defaultCookieManager = new CookieManager();
defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
public static String getUserAgent(Context context) {
String versionName;
try {
......@@ -105,4 +115,11 @@ public class DemoUtil {
return bytes;
}
public static void setDefaultCookieManager() {
CookieHandler currentHandler = CookieHandler.getDefault();
if (currentHandler != defaultCookieManager) {
CookieHandler.setDefault(defaultCookieManager);
}
}
}
......@@ -46,17 +46,17 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&"
+ "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D."
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false,
false),
new Sample("Google Play (DASH)", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A."
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false,
false),
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS_VOD, false, false),
DemoUtil.TYPE_SS, false, false),
new Sample("Dizzy (Misc)", "uid:misc:dizzy",
"http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false),
};
......@@ -66,13 +66,13 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&"
+ "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D."
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false,
true),
new Sample("Google Play", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A."
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false,
true),
};
......@@ -81,21 +81,21 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?"
+ "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=A3EC7EE53ABE601B357F7CAB8B54AD0702CA85A7."
+ "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true),
+ "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, false, true),
new Sample("Google Play", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?"
+ "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=B752B262C6D7262EC4E4EB67901E5D8F7058A81D."
+ "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true),
+ "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, false, true),
};
public static final Sample[] SMOOTHSTREAMING = new Sample[] {
new Sample("Super speed", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS_VOD, false, true),
DemoUtil.TYPE_SS, false, true),
new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS_VOD, true, true),
DemoUtil.TYPE_SS, true, true),
};
public static final Sample[] WIDEVINE_GTS = new Sample[] {
......@@ -103,32 +103,32 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=41EA40A027A125A16292E0A5E3277A3B5FA9B938."
+ "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: HDCP not required", "48fcc369939ac96c",
"http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=315911BDCEED0FB0C763455BDCC97449DAAFA9E8."
+ "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: HDCP required", "e06c39f1151da3df",
"http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=A47A1E13E7243BD567601A75F79B34644D0DC592."
+ "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: Secure video path required", "0894c7c8719b28a0",
"http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=2847EE498970F6B45176766CD2802FEB4D4CB7B2."
+ "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a",
"http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F."
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: 30s license duration", "f9a34cab7b05881a",
"http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true, true),
};
public static final Sample[] MISC = new Sample[] {
......@@ -136,6 +136,9 @@ package com.google.android.exoplayer.demo;
DemoUtil.TYPE_OTHER, false, true),
new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj",
DemoUtil.TYPE_OTHER, false, true),
new Sample("Apple AAC 10s", "uid:misc:appleaacseg", "https://devimages.apple.com.edgekey.net/"
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
DemoUtil.TYPE_OTHER, false, true),
};
private Samples() {}
......
......@@ -16,8 +16,8 @@
package com.google.android.exoplayer.demo.full;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.util.VerboseLogUtil;
......@@ -73,8 +73,8 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@Override
public void onVideoSizeChanged(int width, int height) {
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]");
public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + pixelWidthHeightRatio + "]");
}
// DemoPlayer.InfoListener
......@@ -149,7 +149,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@Override
public void onAudioTrackInitializationError(AudioTrackInitializationException e) {
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
printInternalError("audioTrackInitializationError", e);
}
......
......@@ -19,18 +19,25 @@ import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.VideoSurfaceView;
import com.google.android.exoplayer.demo.DemoUtil;
import com.google.android.exoplayer.demo.R;
import com.google.android.exoplayer.demo.full.player.DashVodRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DashRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView;
import com.google.android.exoplayer.util.Util;
import com.google.android.exoplayer.util.VerboseLogUtil;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Display;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
......@@ -38,6 +45,8 @@ import android.view.SurfaceHolder;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.view.accessibility.CaptioningManager;
import android.widget.Button;
import android.widget.MediaController;
import android.widget.PopupMenu;
......@@ -50,6 +59,7 @@ import android.widget.TextView;
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
DemoPlayer.Listener, DemoPlayer.TextListener {
private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f;
private static final int MENU_GROUP_TRACKS = 1;
private static final int ID_OFFSET = 2;
......@@ -60,7 +70,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
private VideoSurfaceView surfaceView;
private TextView debugTextView;
private TextView playerStateTextView;
private TextView subtitlesTextView;
private SubtitleView subtitleView;
private Button videoButton;
private Button audioButton;
private Button textButton;
......@@ -70,7 +80,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
private boolean playerNeedsPrepare;
private boolean autoPlay = true;
private int playerPosition;
private long playerPosition;
private boolean enableBackgroundAudio = false;
private Uri contentUri;
......@@ -108,7 +118,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
debugTextView = (TextView) findViewById(R.id.debug_text_view);
playerStateTextView = (TextView) findViewById(R.id.player_state_view);
subtitlesTextView = (TextView) findViewById(R.id.subtitles);
subtitleView = (SubtitleView) findViewById(R.id.subtitles);
mediaController = new MediaController(this);
mediaController.setAnchorView(root);
......@@ -117,11 +127,14 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
videoButton = (Button) findViewById(R.id.video_controls);
audioButton = (Button) findViewById(R.id.audio_controls);
textButton = (Button) findViewById(R.id.text_controls);
DemoUtil.setDefaultCookieManager();
}
@Override
public void onResume() {
super.onResume();
configureSubtitleView();
preparePlayer();
}
......@@ -156,11 +169,11 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
private RendererBuilder getRendererBuilder() {
String userAgent = DemoUtil.getUserAgent(this);
switch (contentType) {
case DemoUtil.TYPE_SS_VOD:
case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId,
new SmoothStreamingTestMediaDrmCallback(), debugTextView);
case DemoUtil.TYPE_DASH_VOD:
return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId,
case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
new WidevineTestMediaDrmCallback(contentId), debugTextView);
default:
return new DefaultRendererBuilder(this, contentUri, debugTextView);
......@@ -249,9 +262,10 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
}
@Override
public void onVideoSizeChanged(int width, int height) {
public void onVideoSizeChanged(int width, int height, float pixelWidthAspectRatio) {
shutterView.setVisibility(View.GONE);
surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height);
surfaceView.setVideoWidthHeightRatio(
height == 0 ? 1 : (width * pixelWidthAspectRatio) / height);
}
// User controls
......@@ -380,10 +394,10 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
@Override
public void onText(String text) {
if (TextUtils.isEmpty(text)) {
subtitlesTextView.setVisibility(View.INVISIBLE);
subtitleView.setVisibility(View.INVISIBLE);
} else {
subtitlesTextView.setVisibility(View.VISIBLE);
subtitlesTextView.setText(text);
subtitleView.setVisibility(View.VISIBLE);
subtitleView.setText(text);
}
}
......@@ -409,4 +423,40 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
}
}
private void configureSubtitleView() {
CaptionStyleCompat captionStyle;
float captionTextSize = getCaptionFontSize();
if (Util.SDK_INT >= 19) {
captionStyle = getUserCaptionStyleV19();
captionTextSize *= getUserCaptionFontScaleV19();
} else {
captionStyle = CaptionStyleCompat.DEFAULT;
}
subtitleView.setStyle(captionStyle);
subtitleView.setTextSize(captionTextSize);
}
private float getCaptionFontSize() {
Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
Point displaySize = new Point();
display.getSize(displaySize);
return Math.max(getResources().getDimension(R.dimen.subtitle_minimum_font_size),
CAPTION_LINE_HEIGHT_RATIO * Math.min(displaySize.x, displaySize.y));
}
@TargetApi(19)
private float getUserCaptionFontScaleV19() {
CaptioningManager captioningManager =
(CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
return captioningManager.getFontScale();
}
@TargetApi(19)
private CaptionStyleCompat getUserCaptionStyleV19() {
CaptioningManager captioningManager =
(CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
}
}
......@@ -68,10 +68,10 @@ import android.widget.TextView;
}
@Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
maybeFail();
if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) {
currentPositionUs = timeUs;
if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) {
currentPositionUs = positionUs;
textView.post(this);
}
}
......
......@@ -48,8 +48,8 @@ public class DefaultRendererBuilder implements RendererBuilder {
// Build the video and audio renderers.
FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
player.getMainHandler(), player, 50);
null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(),
player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
null, true, player.getMainHandler(), player);
......
......@@ -19,10 +19,10 @@ import com.google.android.exoplayer.DummyTrackRenderer;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
......@@ -93,7 +93,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public interface Listener {
void onStateChanged(boolean playWhenReady, int playbackState);
void onError(Exception e);
void onVideoSizeChanged(int width, int height);
void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio);
}
/**
......@@ -106,7 +106,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
*/
public interface InternalErrorListener {
void onRendererInitializationError(Exception e);
void onAudioTrackInitializationError(AudioTrackInitializationException e);
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
void onDecoderInitializationError(DecoderInitializationException e);
void onCryptoError(CryptoException e);
void onUpstreamError(int sourceId, IOException e);
......@@ -131,7 +131,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
* A listener for receiving notifications of timed text.
*/
public interface TextListener {
public abstract void onText(String text);
void onText(String text);
}
// Constants pulled into this class for convenience.
......@@ -287,7 +287,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
this.trackNames = trackNames;
this.multiTrackSources = multiTrackSources;
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
maybeReportPlayerState();
pushSurfaceAndVideoTrack(false);
pushTrackSelection(TYPE_AUDIO, true);
pushTrackSelection(TYPE_TEXT, true);
......@@ -310,7 +309,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
player.setPlayWhenReady(playWhenReady);
}
public void seekTo(int positionMs) {
public void seekTo(long positionMs) {
player.seekTo(positionMs);
}
......@@ -339,11 +338,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
return playerState;
}
public int getCurrentPosition() {
public long getCurrentPosition() {
return player.getCurrentPosition();
}
public int getDuration() {
public long getDuration() {
return player.getDuration();
}
......@@ -377,9 +376,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onVideoSizeChanged(int width, int height) {
public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
for (Listener listener : listeners) {
listener.onVideoSizeChanged(width, height);
listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio);
}
}
......@@ -425,7 +424,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onAudioTrackInitializationError(AudioTrackInitializationException e) {
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackInitializationError(e);
}
......
......@@ -35,14 +35,14 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.text.ttml.TtmlParser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
......@@ -51,6 +51,7 @@ import android.media.UnsupportedSchemeException;
import android.os.Handler;
import android.widget.TextView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.UUID;
......@@ -58,12 +59,13 @@ import java.util.UUID;
* A {@link RendererBuilder} for SmoothStreaming.
*/
public class SmoothStreamingRendererBuilder implements RendererBuilder,
ManifestCallback<SmoothStreamingManifest> {
ManifestFetcher.ManifestCallback<SmoothStreamingManifest> {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 60;
private static final int TTML_BUFFER_SEGMENTS = 2;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final String userAgent;
private final String url;
......@@ -73,6 +75,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
private DemoPlayer player;
private RendererBuilderCallback callback;
private ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId,
MediaDrmCallback drmCallback, TextView debugTextView) {
......@@ -87,13 +90,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
this.player = player;
this.callback = callback;
SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this);
mpdFetcher.execute(url + "/Manifest", contentId);
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(parser, contentId,
url + "/Manifest", userAgent);
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
@Override
public void onManifestError(String contentId, Exception e) {
callback.onRenderersError(e);
public void onManifestError(String contentId, IOException exception) {
callback.onRenderersError(exception);
}
@Override
......@@ -144,21 +149,18 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
}
}
}
int[] videoTrackIndices = new int[videoTrackIndexList.size()];
for (int i = 0; i < videoTrackIndexList.size(); i++) {
videoTrackIndices[i] = videoTrackIndexList.get(i);
}
int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null,
mainHandler, player, 50);
// Build the audio renderer.
......@@ -172,14 +174,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else {
audioTrackNames = new String[audioStreamElementCount];
ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
audioStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) {
if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name;
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
i, new int[] {0}, audioDataSource, audioFormatEvaluator);
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(
manifestFetcher, i, new int[] {0}, audioDataSource, audioFormatEvaluator,
LIVE_EDGE_LATENCY_MS);
audioStreamElementCount++;
}
}
......@@ -202,20 +205,20 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else {
textTrackNames = new String[textStreamElementCount];
ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
DataSource ttmlDataSource = new UriDataSource(userAgent, bandwidthMeter);
FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
textStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) {
if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
textTrackNames[textStreamElementCount] = manifest.streamElements[i].language;
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator);
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifestFetcher,
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator, LIVE_EDGE_LATENCY_MS);
textStreamElementCount++;
}
}
textChunkSource = new MultiTrackChunkSource(textChunkSources);
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TTML_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player,
mainHandler.getLooper());
......@@ -249,7 +252,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player,
MediaDrmCallback drmCallback) throws UnsupportedSchemeException {
return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback,
return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, null,
player.getMainHandler(), player);
}
......
......@@ -29,7 +29,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
......@@ -37,24 +37,29 @@ import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBui
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.media.MediaCodec;
import android.os.AsyncTask;
import android.os.Handler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link RendererBuilder} for DASH VOD.
* A {@link RendererBuilder} for DASH.
*/
/* package */ class DashVodRendererBuilder implements RendererBuilder,
/* package */ class DashRendererBuilder implements RendererBuilder,
ManifestCallback<MediaPresentationDescription> {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 60;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final SimplePlayerActivity playerActivity;
private final String userAgent;
......@@ -62,8 +67,9 @@ import java.util.ArrayList;
private final String contentId;
private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
public DashRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
String contentId) {
this.playerActivity = playerActivity;
this.userAgent = userAgent;
......@@ -74,59 +80,63 @@ import java.util.ArrayList;
@Override
public void buildRenderers(RendererBuilderCallback callback) {
this.callback = callback;
MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this);
mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId);
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url,
userAgent);
manifestFetcher.singleLoad(playerActivity.getMainLooper(), this);
}
@Override
public void onManifestError(String contentId, Exception e) {
public void onManifestError(String contentId, IOException e) {
callback.onRenderersError(e);
}
@Override
public void onManifest(String contentId, MediaPresentationDescription manifest) {
Period period = manifest.periods.get(0);
Handler mainHandler = playerActivity.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
// Obtain Representations for playback.
// Determine which video representations we should use for playback.
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
Representation audioRepresentation = null;
ArrayList<Representation> videoRepresentationsList = new ArrayList<Representation>();
Period period = manifest.periods.get(0);
for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
int adaptationSetType = adaptationSet.type;
for (int j = 0; j < adaptationSet.representations.size(); j++) {
Representation representation = adaptationSet.representations.get(j);
if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) {
audioRepresentation = representation;
} else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) {
Format format = representation.format;
if (format.width * format.height <= maxDecodableFrameSize) {
videoRepresentationsList.add(representation);
} else {
// The device isn't capable of playing this stream.
}
}
int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
List<Representation> videoRepresentations =
period.adaptationSets.get(videoAdaptationSetIndex).representations;
ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
for (int i = 0; i < videoRepresentations.size(); i++) {
Format format = videoRepresentations.get(i).format;
if (format.width * format.height > maxDecodableFrameSize) {
// Filtering stream that device cannot play
} else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
&& !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
// Filtering unsupported mime type
} else {
videoRepresentationIndexList.add(i);
}
}
Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()];
videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
final MediaCodecVideoTrackRenderer videoRenderer;
if (videoRepresentationIndexList.isEmpty()) {
videoRenderer = null;
} else {
int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList);
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
}
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new DashChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
......
......@@ -61,10 +61,6 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
private static final String TAG = "PlayerActivity";
public static final int TYPE_DASH_VOD = 0;
public static final int TYPE_SS_VOD = 1;
public static final int TYPE_OTHER = 2;
private MediaController mediaController;
private Handler mainHandler;
private View shutterView;
......@@ -76,7 +72,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
private MediaCodecVideoTrackRenderer videoRenderer;
private boolean autoPlay = true;
private int playerPosition;
private long playerPosition;
private Uri contentUri;
private int contentType;
......@@ -90,7 +86,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
Intent intent = getIntent();
contentUri = intent.getData();
contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER);
contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_OTHER);
contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA);
mainHandler = new Handler(getMainLooper());
......@@ -113,6 +109,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
shutterView = findViewById(R.id.shutter);
surfaceView = (VideoSurfaceView) findViewById(R.id.surface_view);
surfaceView.getHolder().addCallback(this);
DemoUtil.setDefaultCookieManager();
}
@Override
......@@ -163,11 +161,11 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
private RendererBuilder getRendererBuilder() {
String userAgent = DemoUtil.getUserAgent(this);
switch (contentType) {
case TYPE_SS_VOD:
case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
contentId);
case TYPE_DASH_VOD:
return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId);
case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
default:
return new DefaultRendererBuilder(this, contentUri);
}
......@@ -231,8 +229,9 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
// MediaCodecVideoTrackRenderer.Listener
@Override
public void onVideoSizeChanged(int width, int height) {
surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height);
public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
surfaceView.setVideoWidthHeightRatio(
height == 0 ? 1 : (pixelWidthHeightRatio * width) / height);
}
@Override
......
......@@ -31,16 +31,19 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import com.google.android.exoplayer.util.Util;
import android.media.MediaCodec;
import android.os.Handler;
import java.io.IOException;
import java.util.ArrayList;
/**
......@@ -52,6 +55,7 @@ import java.util.ArrayList;
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 60;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final SimplePlayerActivity playerActivity;
private final String userAgent;
......@@ -59,6 +63,7 @@ import java.util.ArrayList;
private final String contentId;
private RendererBuilderCallback callback;
private ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
public SmoothStreamingRendererBuilder(SimplePlayerActivity playerActivity, String userAgent,
String url, String contentId) {
......@@ -71,12 +76,14 @@ import java.util.ArrayList;
@Override
public void buildRenderers(RendererBuilderCallback callback) {
this.callback = callback;
SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this);
mpdFetcher.execute(url + "/Manifest", contentId);
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(parser, contentId,
url + "/Manifest", userAgent);
manifestFetcher.singleLoad(playerActivity.getMainLooper(), this);
}
@Override
public void onManifestError(String contentId, Exception e) {
public void onManifestError(String contentId, IOException e) {
callback.onRenderersError(e);
}
......@@ -109,26 +116,23 @@ import java.util.ArrayList;
}
}
}
int[] videoTrackIndices = new int[videoTrackIndexList.size()];
for (int i = 0; i < videoTrackIndexList.size(); i++) {
videoTrackIndices[i] = videoTrackIndexList.get(i);
}
int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
audioStreamElementIndex, new int[] {0}, audioDataSource,
new FormatEvaluator.FixedEvaluator());
new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
......
......@@ -8,6 +8,6 @@
# project structure.
# Project target.
target=android-19
target=android-21
android.library=false
android.library.reference.1=../../../library/src/main
......@@ -24,15 +24,13 @@
android:layout_height="match_parent"
android:layout_gravity="center"/>
<TextView android:id="@+id/subtitles"
android:layout_width="match_parent"
<com.google.android.exoplayer.text.SubtitleView android:id="@+id/subtitles"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="32dp"
android:gravity="center"
android:textSize="20sp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="32dp"
android:visibility="invisible"/>
<View android:id="@+id/shutter"
......
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The minimum subtitle font size. -->
<dimen name="subtitle_minimum_font_size">13sp</dimen>
</resources>
#Tue Jun 10 20:02:28 BST 2014
#Thu Nov 20 12:15:03 PST 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-rc-1-bin.zip
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerLib</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>1363908161147</id>
<name></name>
<type>22</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-false-false-BUILD</arguments>
</matcher>
</filter>
<filter>
<id>1363908161148</id>
<name></name>
<type>10</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-build</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
......@@ -11,20 +11,20 @@
// 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.
apply plugin: 'android-library'
apply plugin: 'com.android.library'
android {
compileSdkVersion 19
buildToolsVersion "19.1"
compileSdkVersion 21
buildToolsVersion "21.1.1"
defaultConfig {
minSdkVersion 9
targetSdkVersion 19
targetSdkVersion 21
}
buildTypes {
release {
runProguard false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
......
This diff could not be displayed because it is too large.
......@@ -30,24 +30,4 @@
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>1363908161147</id>
<name></name>
<type>22</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-false-false-BUILD</arguments>
</matcher>
</filter>
<filter>
<id>1363908161148</id>
<name></name>
<type>10</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-build</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
......@@ -27,6 +27,6 @@
the library may be of use on older devices. However, please note that the core video playback
functionality provided by the library requires API level 16 or greater.
-->
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/>
</manifest>
/*
* 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;
import java.io.IOException;
/**
* Thrown when a live playback falls behind the available media window.
*/
public class BehindLiveWindowException extends IOException {
public BehindLiveWindowException() {
super();
}
public BehindLiveWindowException(String message) {
super(message);
}
}
......@@ -21,10 +21,25 @@ package com.google.android.exoplayer;
public final class C {
/**
* Represents an unknown microsecond time or duration.
*/
public static final long UNKNOWN_TIME_US = -1L;
/**
* The number of microseconds in one second.
*/
public static final long MICROS_PER_SECOND = 1000000L;
/**
* Represents an unbounded length of data.
*/
public static final int LENGTH_UNBOUNDED = -1;
/**
* The name of the UTF-8 charset.
*/
public static final String UTF8_NAME = "UTF-8";
private C() {}
}
......@@ -29,7 +29,7 @@ public final class DecoderInfo {
public final String name;
/**
* Whether the decoder is adaptive.
* Whether the decoder supports seamless resolution switches.
*
* @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String)
* @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback
......
......@@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl {
// Update the loader state.
int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs);
LoaderState loaderState = loaderStates.get(loader);
boolean loaderStateChanged = loaderState.bufferState != loaderBufferState ||
loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading ||
loaderState.failed != failed;
boolean loaderStateChanged = loaderState.bufferState != loaderBufferState
|| loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading
|| loaderState.failed != failed;
if (loaderStateChanged) {
loaderState.bufferState = loaderBufferState;
loaderState.nextLoadPositionUs = nextLoadPositionUs;
......@@ -214,17 +214,17 @@ public class DefaultLoadControl implements LoadControl {
private void updateControlState() {
boolean loading = false;
boolean failed = false;
boolean finished = true;
boolean haveNextLoadPosition = false;
int highestState = bufferPoolState;
for (int i = 0; i < loaders.size(); i++) {
LoaderState loaderState = loaderStates.get(loaders.get(i));
loading |= loaderState.loading;
failed |= loaderState.failed;
finished &= loaderState.nextLoadPositionUs == -1;
haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1;
highestState = Math.max(highestState, loaderState.bufferState);
}
fillingBuffers = !loaders.isEmpty() && !finished && !failed
fillingBuffers = !loaders.isEmpty() && !failed && (loading || haveNextLoadPosition)
&& (highestState == BELOW_LOW_WATERMARK
|| (highestState == BETWEEN_WATERMARKS && fillingBuffers));
if (fillingBuffers && !streamingPrioritySet) {
......
......@@ -40,12 +40,12 @@ public class DummyTrackRenderer extends TrackRenderer {
}
@Override
protected void seekTo(long timeUs) {
protected void seekTo(long positionUs) {
throw new IllegalStateException();
}
@Override
protected void doSomeWork(long timeUs) {
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) {
throw new IllegalStateException();
}
......
......@@ -229,7 +229,7 @@ public interface ExoPlayer {
/**
* Represents an unknown time or duration.
*/
public static final int UNKNOWN_TIME = -1;
public static final long UNKNOWN_TIME = -1;
/**
* Gets the {@link Looper} associated with the playback thread.
......@@ -313,7 +313,7 @@ public interface ExoPlayer {
*
* @param positionMs The seek position.
*/
public void seekTo(int positionMs);
public void seekTo(long positionMs);
/**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
......@@ -363,14 +363,14 @@ public interface ExoPlayer {
* @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the
* duration is not known.
*/
public int getDuration();
public long getDuration();
/**
* Gets the current playback position in milliseconds.
*
* @return The current playback position in milliseconds.
*/
public int getCurrentPosition();
public long getCurrentPosition();
/**
* Gets an estimate of the absolute position in milliseconds up to which data is buffered.
......@@ -378,7 +378,7 @@ public interface ExoPlayer {
* @return An estimate of the absolute position in milliseconds up to which data is buffered,
* or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available.
*/
public int getBufferedPosition();
public long getBufferedPosition();
/**
* Gets an estimate of the percentage into the media up to which data is buffered.
......
......@@ -130,7 +130,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Override
public void seekTo(int positionMs) {
public void seekTo(long positionMs) {
internalPlayer.seekTo(positionMs);
}
......@@ -156,26 +156,26 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Override
public int getDuration() {
public long getDuration() {
return internalPlayer.getDuration();
}
@Override
public int getCurrentPosition() {
public long getCurrentPosition() {
return internalPlayer.getCurrentPosition();
}
@Override
public int getBufferedPosition() {
public long getBufferedPosition() {
return internalPlayer.getBufferedPosition();
}
@Override
public int getBufferedPercentage() {
int bufferedPosition = getBufferedPosition();
int duration = getDuration();
long bufferedPosition = getBufferedPosition();
long duration = getDuration();
return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0
: (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
}
// Not private so it can be called from an inner class without going through a thunk method.
......
......@@ -17,9 +17,9 @@ package com.google.android.exoplayer;
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.PriorityHandlerThread;
import com.google.android.exoplayer.util.TraceUtil;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
......@@ -77,12 +77,12 @@ import java.util.List;
private int state;
private int customMessagesSent = 0;
private int customMessagesProcessed = 0;
private long elapsedRealtimeUs;
private volatile long durationUs;
private volatile long positionUs;
private volatile long bufferedPositionUs;
@SuppressLint("HandlerLeak")
public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady,
boolean[] rendererEnabledFlags, int minBufferMs, int minRebufferMs) {
this.eventHandler = eventHandler;
......@@ -100,15 +100,10 @@ import java.util.List;
mediaClock = new MediaClock();
enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
internalPlaybackThread = new HandlerThread(getClass().getSimpleName() + ":Handler") {
@Override
public void run() {
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
super.run();
}
};
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler",
Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
handler = new Handler(internalPlaybackThread.getLooper(), this);
}
......@@ -117,18 +112,18 @@ import java.util.List;
return internalPlaybackThread.getLooper();
}
public int getCurrentPosition() {
return (int) (positionUs / 1000);
public long getCurrentPosition() {
return positionUs / 1000;
}
public int getBufferedPosition() {
public long getBufferedPosition() {
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: (int) (bufferedPositionUs / 1000);
: bufferedPositionUs / 1000;
}
public int getDuration() {
public long getDuration() {
return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: (int) (durationUs / 1000);
: durationUs / 1000;
}
public void prepare(TrackRenderer... renderers) {
......@@ -139,8 +134,8 @@ import java.util.List;
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
}
public void seekTo(int positionMs) {
handler.obtainMessage(MSG_SEEK_TO, positionMs, 0).sendToTarget();
public void seekTo(long positionMs) {
handler.obtainMessage(MSG_SEEK_TO, positionMs).sendToTarget();
}
public void stop() {
......@@ -158,6 +153,10 @@ import java.util.List;
public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType,
Object message) {
if (released) {
Log.w(TAG, "Sent message(" + messageType + ") after release. Message ignored.");
return;
}
int messageNumber = customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
while (customMessagesProcessed <= messageNumber) {
......@@ -170,17 +169,18 @@ import java.util.List;
}
public synchronized void release() {
if (!released) {
handler.sendEmptyMessage(MSG_RELEASE);
while (!released) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (released) {
return;
}
handler.sendEmptyMessage(MSG_RELEASE);
while (!released) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
internalPlaybackThread.quit();
}
internalPlaybackThread.quit();
}
@Override
......@@ -204,7 +204,7 @@ import java.util.List;
return true;
}
case MSG_SEEK_TO: {
seekToInternal(msg.arg1);
seekToInternal((Long) msg.obj);
return true;
}
case MSG_STOP: {
......@@ -378,7 +378,8 @@ import java.util.List;
positionUs = timeSourceTrackRenderer != null &&
enabledRenderers.contains(timeSourceTrackRenderer) ?
timeSourceTrackRenderer.getCurrentPositionUs() :
mediaClock.getTimeUs();
mediaClock.getPositionUs();
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
}
private void doSomeWork() throws ExoPlaybackException {
......@@ -394,7 +395,7 @@ import java.util.List;
// TODO: Each renderer should return the maximum delay before which it wishes to be
// invoked again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.doSomeWork(positionUs);
renderer.doSomeWork(positionUs, elapsedRealtimeUs);
isEnded = isEnded && renderer.isEnded();
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
......@@ -453,11 +454,11 @@ import java.util.List;
}
}
private void seekToInternal(int positionMs) throws ExoPlaybackException {
private void seekToInternal(long positionMs) throws ExoPlaybackException {
rebuffering = false;
positionUs = positionMs * 1000L;
mediaClock.stop();
mediaClock.setTimeUs(positionUs);
mediaClock.setPositionUs(positionUs);
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
return;
}
......@@ -491,21 +492,9 @@ import java.util.List;
return;
}
for (int i = 0; i < renderers.length; i++) {
try {
TrackRenderer renderer = renderers[i];
ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
renderer.disable();
}
renderer.release();
} catch (ExoPlaybackException e) {
// There's nothing we can do. Catch the exception here so that other renderers still have
// a chance of being cleaned up correctly.
Log.e(TAG, "Stop failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Stop failed.", e);
}
TrackRenderer renderer = renderers[i];
stopAndDisable(renderer);
release(renderer);
}
renderers = null;
timeSourceTrackRenderer = null;
......@@ -513,6 +502,33 @@ import java.util.List;
setState(ExoPlayer.STATE_IDLE);
}
private void stopAndDisable(TrackRenderer renderer) {
try {
ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
renderer.disable();
}
} catch (ExoPlaybackException e) {
// There's nothing we can do.
Log.e(TAG, "Stop failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Stop failed.", e);
}
}
private void release(TrackRenderer renderer) {
try {
renderer.release();
} catch (ExoPlaybackException e) {
// There's nothing we can do.
Log.e(TAG, "Release failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Release failed.", e);
}
}
private <T> void sendMessageInternal(int what, Object obj)
throws ExoPlaybackException {
try {
......@@ -562,7 +578,7 @@ import java.util.List;
if (renderer == timeSourceTrackRenderer) {
// We've been using timeSourceTrackRenderer to advance the current position, but it's
// being disabled. Sync mediaClock so that it can take over timing responsibilities.
mediaClock.setTimeUs(renderer.getCurrentPositionUs());
mediaClock.setPositionUs(renderer.getCurrentPositionUs());
}
ensureStopped(renderer);
enabledRenderers.remove(renderer);
......
......@@ -26,15 +26,15 @@ public class ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
public static final String VERSION = "1.0.13";
public static final String VERSION = "1.1.0";
/**
* The version of the library, expressed as an integer.
* <p>
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 1002003.
* corresponding integer version 001002003.
*/
public static final int VERSION_INT = 1000013;
public static final int VERSION_INT = 001001000;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
......
......@@ -50,7 +50,7 @@ public final class FrameworkSampleSource implements SampleSource {
private int[] trackStates;
private boolean[] pendingDiscontinuities;
private long seekTimeUs;
private long seekPositionUs;
public FrameworkSampleSource(Context context, Uri uri, Map<String, String> headers,
int downstreamRendererCount) {
......@@ -71,10 +71,10 @@ public final class FrameworkSampleSource implements SampleSource {
trackInfos = new TrackInfo[trackStates.length];
for (int i = 0; i < trackStates.length; i++) {
android.media.MediaFormat format = extractor.getTrackFormat(i);
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME_US;
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, duration);
trackInfos[i] = new TrackInfo(mime, durationUs);
}
prepared = true;
}
......@@ -94,16 +94,16 @@ public final class FrameworkSampleSource implements SampleSource {
}
@Override
public void enable(int track, long timeUs) {
public void enable(int track, long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
trackStates[track] = TRACK_STATE_ENABLED;
extractor.selectTrack(track);
seekToUs(timeUs);
seekToUs(positionUs);
}
@Override
public boolean continueBuffering(long playbackPositionUs) {
public boolean continueBuffering(long positionUs) {
// MediaExtractor takes care of buffering and blocks until it has samples, so we can always
// return true here. Although note that the blocking behavior is itself as bug, as per the
// TODO further up this file. This method will need to return something else as part of fixing
......@@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource {
}
@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
......@@ -144,7 +144,7 @@ public final class FrameworkSampleSource implements SampleSource {
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
sampleHolder.cryptoInfo.setFromExtractorV16(extractor);
}
seekTimeUs = -1;
seekPositionUs = -1;
extractor.advance();
return SAMPLE_READ;
} else {
......@@ -168,13 +168,13 @@ public final class FrameworkSampleSource implements SampleSource {
}
@Override
public void seekToUs(long timeUs) {
public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
if (seekTimeUs != timeUs) {
if (seekPositionUs != positionUs) {
// Avoid duplicate calls to the underlying extractor's seek method in the case that there
// have been no interleaving calls to advance.
seekTimeUs = timeUs;
extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
seekPositionUs = positionUs;
extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
for (int i = 0; i < trackStates.length; ++i) {
if (trackStates[i] != TRACK_STATE_DISABLED) {
pendingDiscontinuities[i] = true;
......
......@@ -65,9 +65,10 @@ public interface LoadControl {
*
* @param loader The loader invoking the update.
* @param playbackPositionUs The loader's playback position.
* @param nextLoadPositionUs The loader's next load position, or -1 if finished.
* @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the
* next load position is not yet known.
* @param loading Whether the loader is currently loading data.
* @param failed Whether the loader has failed, meaning it does not wish to load more data.
* @param failed Whether the loader has failed.
* @return True if the loader is allowed to start its next load. False otherwise.
*/
boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
......
......@@ -29,10 +29,10 @@ import android.os.SystemClock;
/**
* The media time when the clock was last set or stopped.
*/
private long timeUs;
private long positionUs;
/**
* The difference between {@link SystemClock#elapsedRealtime()} and {@link #timeUs}
* The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs}
* when the clock was last set or started.
*/
private long deltaUs;
......@@ -43,7 +43,7 @@ import android.os.SystemClock;
public void start() {
if (!started) {
started = true;
deltaUs = elapsedRealtimeMinus(timeUs);
deltaUs = elapsedRealtimeMinus(positionUs);
}
}
......@@ -52,28 +52,28 @@ import android.os.SystemClock;
*/
public void stop() {
if (started) {
timeUs = elapsedRealtimeMinus(deltaUs);
positionUs = elapsedRealtimeMinus(deltaUs);
started = false;
}
}
/**
* @param timeUs The time to set in microseconds.
* @param timeUs The position to set in microseconds.
*/
public void setTimeUs(long timeUs) {
this.timeUs = timeUs;
public void setPositionUs(long timeUs) {
this.positionUs = timeUs;
deltaUs = elapsedRealtimeMinus(timeUs);
}
/**
* @return The current time in microseconds.
* @return The current position in microseconds.
*/
public long getTimeUs() {
return started ? elapsedRealtimeMinus(deltaUs) : timeUs;
public long getPositionUs() {
return started ? elapsedRealtimeMinus(deltaUs) : positionUs;
}
private long elapsedRealtimeMinus(long microSeconds) {
return SystemClock.elapsedRealtime() * 1000 - microSeconds;
private long elapsedRealtimeMinus(long toSubtractUs) {
return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
}
}
......@@ -31,6 +31,9 @@ import java.util.List;
*/
public class MediaFormat {
private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
"com.google.android.videos.pixelWidthHeightRatio";
public static final int NO_VALUE = -1;
public final String mimeType;
......@@ -38,10 +41,13 @@ public class MediaFormat {
public final int width;
public final int height;
public final float pixelWidthHeightRatio;
public final int channelCount;
public final int sampleRate;
public final int bitrate;
private int maxWidth;
private int maxHeight;
......@@ -59,14 +65,25 @@ public class MediaFormat {
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, NO_VALUE, NO_VALUE,
initializationData);
return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData);
}
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE,
NO_VALUE, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, channelCount, sampleRate,
initializationData);
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
sampleRate, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, int bitrate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
sampleRate, bitrate, initializationData);
}
@TargetApi(16)
......@@ -78,6 +95,8 @@ public class MediaFormat {
height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
bitrate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_BIT_RATE);
pixelWidthHeightRatio = getOptionalFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
initializationData = new ArrayList<byte[]>();
for (int i = 0; format.containsKey("csd-" + i); i++) {
ByteBuffer buffer = format.getByteBuffer("csd-" + i);
......@@ -90,14 +109,17 @@ public class MediaFormat {
maxHeight = NO_VALUE;
}
private MediaFormat(String mimeType, int maxInputSize, int width, int height, int channelCount,
int sampleRate, List<byte[]> initializationData) {
private MediaFormat(String mimeType, int maxInputSize, int width, int height,
float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
List<byte[]> initializationData) {
this.mimeType = mimeType;
this.maxInputSize = maxInputSize;
this.width = width;
this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.bitrate = bitrate;
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
: initializationData;
maxWidth = NO_VALUE;
......@@ -128,10 +150,12 @@ public class MediaFormat {
result = 31 * result + maxInputSize;
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + maxWidth;
result = 31 * result + maxHeight;
result = 31 * result + channelCount;
result = 31 * result + sampleRate;
result = 31 * result + bitrate;
for (int i = 0; i < initializationData.size(); i++) {
result = 31 * result + Arrays.hashCode(initializationData.get(i));
}
......@@ -163,9 +187,11 @@ public class MediaFormat {
private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) {
if (maxInputSize != other.maxInputSize || width != other.width || height != other.height
|| pixelWidthHeightRatio != other.pixelWidthHeightRatio
|| (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight))
|| channelCount != other.channelCount || sampleRate != other.sampleRate
|| !Util.areEqual(mimeType, other.mimeType)
|| bitrate != other.bitrate
|| initializationData.size() != other.initializationData.size()) {
return false;
}
......@@ -179,8 +205,9 @@ public class MediaFormat {
@Override
public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " +
channelCount + ", " + sampleRate + ", " + maxWidth + ", " + maxHeight + ")";
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
+ maxWidth + ", " + maxHeight + ")";
}
/**
......@@ -196,6 +223,8 @@ public class MediaFormat {
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_BIT_RATE, bitrate);
maybeSetFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio);
for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
......@@ -221,9 +250,21 @@ public class MediaFormat {
}
@TargetApi(16)
private static final int getOptionalIntegerV16(android.media.MediaFormat format,
String key) {
private static final void maybeSetFloatV16(android.media.MediaFormat format, String key,
float value) {
if (value != NO_VALUE) {
format.setFloat(key, value);
}
}
@TargetApi(16)
private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getInteger(key) : NO_VALUE;
}
@TargetApi(16)
private static final float getOptionalFloatV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getFloat(key) : NO_VALUE;
}
}
......@@ -26,8 +26,12 @@ public class ParserException extends IOException {
super(message);
}
public ParserException(Exception cause) {
public ParserException(Throwable cause) {
super(cause);
}
public ParserException(String message, Throwable cause) {
super(message, cause);
}
}
......@@ -23,10 +23,19 @@ import java.nio.ByteBuffer;
public final class SampleHolder {
/**
* Whether a {@link SampleSource} is permitted to replace {@link #data} if its current value is
* null or of insufficient size to hold the sample.
* Disallows buffer replacement.
*/
public final boolean allowDataBufferReplacement;
public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;
/**
* Allows buffer replacement using {@link ByteBuffer#allocate(int)}.
*/
public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;
/**
* Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.
*/
public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2;
public final CryptoInfo cryptoInfo;
......@@ -57,12 +66,34 @@ public final class SampleHolder {
*/
public boolean decodeOnly;
private final int bufferReplacementMode;
/**
* @param allowDataBufferReplacement See {@link #allowDataBufferReplacement}.
* @param bufferReplacementMode Determines the behavior of {@link #replaceBuffer(int)}. One of
* {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and
* {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.
*/
public SampleHolder(boolean allowDataBufferReplacement) {
public SampleHolder(int bufferReplacementMode) {
this.cryptoInfo = new CryptoInfo();
this.allowDataBufferReplacement = allowDataBufferReplacement;
this.bufferReplacementMode = bufferReplacementMode;
}
/**
* Attempts to replace {@link #data} with a {@link ByteBuffer} of the specified capacity.
*
* @param capacity The capacity of the replacement buffer, in bytes.
* @return True if the buffer was replaced. False otherwise.
*/
public boolean replaceBuffer(int capacity) {
switch (bufferReplacementMode) {
case BUFFER_REPLACEMENT_MODE_NORMAL:
data = ByteBuffer.allocate(capacity);
return true;
case BUFFER_REPLACEMENT_MODE_DIRECT:
data = ByteBuffer.allocateDirect(capacity);
return true;
}
return false;
}
}
......@@ -85,9 +85,9 @@ public interface SampleSource {
* This method should not be called until after the source has been successfully prepared.
*
* @param track The track to enable.
* @param timeUs The player's current playback position.
* @param positionUs The player's current playback position.
*/
public void enable(int track, long timeUs);
public void enable(int track, long positionUs);
/**
* Disable the specified track.
......@@ -101,12 +101,12 @@ public interface SampleSource {
/**
* Indicates to the source that it should still be buffering data.
*
* @param playbackPositionUs The current playback position.
* @param positionUs The current playback position.
* @return True if the source has available samples, or if the end of the stream has been reached.
* False if more data needs to be buffered for samples to become available.
* @throws IOException If an error occurred reading from the source.
*/
public boolean continueBuffering(long playbackPositionUs) throws IOException;
public boolean continueBuffering(long positionUs) throws IOException;
/**
* Attempts to read either a sample, a new format or or a discontinuity from the source.
......@@ -118,7 +118,7 @@ public interface SampleSource {
* than the one for which data was requested.
*
* @param track The track from which to read.
* @param playbackPositionUs The current playback position.
* @param positionUs The current playback position.
* @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
......@@ -129,7 +129,7 @@ public interface SampleSource {
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
* @throws IOException If an error occurred reading from the source.
*/
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
/**
......@@ -137,16 +137,16 @@ public interface SampleSource {
* <p>
* This method should not be called until after the source has been successfully prepared.
*
* @param timeUs The seek position in microseconds.
* @param positionUs The seek position in microseconds.
*/
public void seekToUs(long timeUs);
public void seekToUs(long positionUs);
/**
* Returns an estimate of the position up to which data is buffered.
* <p>
* This method should not be called until after the source has been successfully prepared.
*
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
* @return An estimate of the absolute position in microseconds up to which data is buffered,
* or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or
* {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
*/
......
/*
* 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;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer.FrameReleaseTimeHelper;
import android.annotation.TargetApi;
import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;
/**
* Makes a best effort to adjust frame release timestamps for a smoother visual result.
*/
@TargetApi(16)
public class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelper, FrameCallback {
private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
private static final long VSYNC_OFFSET_PERCENTAGE = 80;
private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
private final boolean usePrimaryDisplayVsync;
private final long vsyncDurationNs;
private final long vsyncOffsetNs;
private Choreographer choreographer;
private long sampledVsyncTimeNs;
private long lastUnadjustedFrameTimeUs;
private long adjustedLastFrameTimeNs;
private long pendingAdjustedFrameTimeNs;
private boolean haveSync;
private long syncReleaseTimeNs;
private long syncFrameTimeNs;
private int frameCount;
/**
* @param primaryDisplayRefreshRate The refresh rate of the default display.
* @param usePrimaryDisplayVsync Whether to snap to the primary display vsync. May not be
* suitable when rendering to secondary displays.
*/
public SmoothFrameReleaseTimeHelper(
float primaryDisplayRefreshRate, boolean usePrimaryDisplayVsync) {
this.usePrimaryDisplayVsync = usePrimaryDisplayVsync;
if (usePrimaryDisplayVsync) {
vsyncDurationNs = (long) (1000000000d / primaryDisplayRefreshRate);
vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
} else {
vsyncDurationNs = -1;
vsyncOffsetNs = -1;
}
}
@Override
public void enable() {
haveSync = false;
if (usePrimaryDisplayVsync) {
sampledVsyncTimeNs = 0;
choreographer = Choreographer.getInstance();
choreographer.postFrameCallback(this);
}
}
@Override
public void disable() {
if (usePrimaryDisplayVsync) {
choreographer.removeFrameCallback(this);
choreographer = null;
}
}
@Override
public void doFrame(long vsyncTimeNs) {
sampledVsyncTimeNs = vsyncTimeNs;
choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
}
@Override
public long adjustReleaseTime(long unadjustedFrameTimeUs, long unadjustedReleaseTimeNs) {
long unadjustedFrameTimeNs = unadjustedFrameTimeUs * 1000;
// Until we know better, the adjustment will be a no-op.
long adjustedFrameTimeNs = unadjustedFrameTimeNs;
long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
if (haveSync) {
// See if we've advanced to the next frame.
if (unadjustedFrameTimeUs != lastUnadjustedFrameTimeUs) {
frameCount++;
adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
}
if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
// We're synced and have waited the required number of frames to apply an adjustment.
// Calculate the average frame time across all the frames we've seen since the last sync.
// This will typically give us a framerate at a finer granularity than the frame times
// themselves (which often only have millisecond granularity).
long averageFrameTimeNs = (unadjustedFrameTimeNs - syncFrameTimeNs) / frameCount;
// Project the adjusted frame time forward using the average.
long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameTimeNs;
if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
haveSync = false;
} else {
adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
adjustedReleaseTimeNs = syncReleaseTimeNs + adjustedFrameTimeNs - syncFrameTimeNs;
}
} else {
// We're synced but haven't waited the required number of frames to apply an adjustment.
// Check drift anyway.
if (isDriftTooLarge(unadjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
haveSync = false;
}
}
}
// If we need to sync, do so now.
if (!haveSync) {
syncFrameTimeNs = unadjustedFrameTimeNs;
syncReleaseTimeNs = unadjustedReleaseTimeNs;
frameCount = 0;
haveSync = true;
onSynced();
}
lastUnadjustedFrameTimeUs = unadjustedFrameTimeUs;
pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
if (sampledVsyncTimeNs == 0) {
return adjustedReleaseTimeNs;
}
// Find the timestamp of the closest vsync. This is the vsync that we're targeting.
long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs);
// Apply an offset so that we release before the target vsync, but after the previous one.
return snappedTimeNs - vsyncOffsetNs;
}
protected void onSynced() {
// Do nothing.
}
private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
long elapsedFrameTimeNs = frameTimeNs - syncFrameTimeNs;
long elapsedReleaseTimeNs = releaseTimeNs - syncReleaseTimeNs;
return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
}
private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
long snappedBeforeNs;
long snappedAfterNs;
if (releaseTime <= snappedTimeNs) {
snappedBeforeNs = snappedTimeNs - vsyncDuration;
snappedAfterNs = snappedTimeNs;
} else {
snappedBeforeNs = snappedTimeNs;
snappedAfterNs = snappedTimeNs + vsyncDuration;
}
long snappedAfterDiff = snappedAfterNs - releaseTime;
long snappedBeforeDiff = releaseTime - snappedBeforeNs;
return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
}
}
......@@ -20,9 +20,21 @@ package com.google.android.exoplayer;
*/
public final class TrackInfo {
/**
* The mime type.
*/
public final String mimeType;
/**
* The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown.
*/
public final long durationUs;
/**
* @param mimeType The mime type.
* @param durationUs The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration
* is unknown.
*/
public TrackInfo(String mimeType, long durationUs) {
this.mimeType = mimeType;
this.durationUs = durationUs;
......
......@@ -18,6 +18,8 @@ package com.google.android.exoplayer;
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer.util.Assertions;
import android.os.SystemClock;
/**
* Renders a single component of media.
*
......@@ -59,15 +61,15 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
*/
protected static final int STATE_ENABLED = 2;
/**
* The renderer is started. Calls to {@link #doSomeWork(long)} should cause the media to be
* The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be
* rendered.
*/
protected static final int STATE_STARTED = 3;
/**
* Represents an unknown time or duration.
* Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}.
*/
public static final long UNKNOWN_TIME_US = -1;
public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1
/**
* Represents a time or duration that should match the duration of the longest track whose
* duration is known.
......@@ -83,9 +85,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
/**
* A time source renderer is a renderer that, when started, advances its own playback position.
* This means that {@link #getCurrentPositionUs()} will return increasing positions independently
* to increasing values being passed to {@link #doSomeWork(long)}. A player may have at most one
* time source renderer. If provided, the player will use such a renderer as its source of time
* during playback.
* to increasing values being passed to {@link #doSomeWork(long, long)}. A player may have at most
* one time source renderer. If provided, the player will use such a renderer as its source of
* time during playback.
* <p>
* This method may be called when the renderer is in any state.
*
......@@ -136,15 +138,15 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
/**
* Enable the renderer.
*
* @param timeUs The player's current position.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback. If true
* then {@link #start} must be called immediately after this method returns (unless a
* {@link ExoPlaybackException} is thrown).
*/
/* package */ final void enable(long timeUs, boolean joining) throws ExoPlaybackException {
/* package */ final void enable(long positionUs, boolean joining) throws ExoPlaybackException {
Assertions.checkState(state == TrackRenderer.STATE_PREPARED);
state = TrackRenderer.STATE_ENABLED;
onEnabled(timeUs, joining);
onEnabled(positionUs, joining);
}
/**
......@@ -152,18 +154,18 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* <p>
* The default implementation is a no-op.
*
* @param timeUs The player's current position.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback. If true
* then {@link #onStarted} is guaranteed to be called immediately after this method returns
* (unless a {@link ExoPlaybackException} is thrown).
* @throws ExoPlaybackException If an error occurs.
*/
protected void onEnabled(long timeUs, boolean joining) throws ExoPlaybackException {
protected void onEnabled(long positionUs, boolean joining) throws ExoPlaybackException {
// Do nothing.
}
/**
* Starts the renderer, meaning that calls to {@link #doSomeWork(long)} will cause the
* Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the
* track to be rendered.
*/
/* package */ final void start() throws ExoPlaybackException {
......@@ -289,10 +291,14 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @param timeUs The current playback time.
* @param positionUs The current media time in microseconds, measured at the start of the
* current iteration of the rendering loop.
* @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at
* the start of the current iteration of the rendering loop.
* @throws ExoPlaybackException If an error occurs.
*/
protected abstract void doSomeWork(long timeUs) throws ExoPlaybackException;
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException;
/**
* Returns the duration of the media being rendered.
......@@ -300,7 +306,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method may be called when the renderer is in the following states:
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST_US} if
* @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if
* the track's duration should match that of the longest track whose duration is known, or
* or {@link #UNKNOWN_TIME_US} if the duration is not known.
*/
......@@ -312,17 +318,17 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return The current playback position in micro-seconds.
* @return The current playback position in microseconds.
*/
protected abstract long getCurrentPositionUs();
/**
* Returns an estimate of the absolute position in micro-seconds up to which data is buffered.
* Returns an estimate of the absolute position in microseconds up to which data is buffered.
* <p>
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
*
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
* @return An estimate of the absolute position in microseconds up to which data is buffered,
* or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if
* no estimate is available.
*/
......@@ -334,10 +340,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}
*
* @param timeUs The desired time in micro-seconds.
* @param positionUs The desired playback position in microseconds.
* @throws ExoPlaybackException If an error occurs.
*/
protected abstract void seekTo(long timeUs) throws ExoPlaybackException;
protected abstract void seekTo(long positionUs) throws ExoPlaybackException;
@Override
public void handleMessage(int what, Object object) throws ExoPlaybackException {
......
/*
* 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.audio;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import java.util.HashSet;
import java.util.Set;
/**
* Represents the set of audio formats a device is capable of playing back.
*/
@TargetApi(21)
public final class AudioCapabilities {
private final Set<Integer> supportedEncodings;
private final int maxChannelCount;
/**
* Constructs new audio capabilities based on a set of supported encodings and a maximum channel
* count.
*
* @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
* {@code ENCODING_*} constants.
* @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
*/
public AudioCapabilities(int[] supportedEncodings, int maxChannelCount) {
this.supportedEncodings = new HashSet<Integer>();
if (supportedEncodings != null) {
for (int i : supportedEncodings) {
this.supportedEncodings.add(i);
}
}
this.maxChannelCount = maxChannelCount;
}
/** Returns whether the device supports playback of AC-3. */
public boolean supportsAc3() {
return Util.SDK_INT >= 21 && supportedEncodings.contains(AudioFormat.ENCODING_AC3);
}
/** Returns whether the device supports playback of enhanced AC-3. */
public boolean supportsEAc3() {
return Util.SDK_INT >= 21 && supportedEncodings.contains(AudioFormat.ENCODING_E_AC3);
}
/** Returns whether the device supports playback of 16-bit PCM. */
public boolean supportsPcm() {
return supportedEncodings.contains(AudioFormat.ENCODING_PCM_16BIT);
}
/** Returns the maximum number of channels the device can play at the same time. */
public int getMaxChannelCount() {
return maxChannelCount;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof AudioCapabilities)) {
return false;
}
AudioCapabilities audioCapabilities = (AudioCapabilities) other;
return supportedEncodings.equals(audioCapabilities.supportedEncodings)
&& maxChannelCount == audioCapabilities.maxChannelCount;
}
@Override
public int hashCode() {
return maxChannelCount + 31 * supportedEncodings.hashCode();
}
@Override
public String toString() {
return "AudioCapabilities[maxChannelCount=" + maxChannelCount
+ ", supportedEncodings=" + supportedEncodings + "]";
}
}
/*
* 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.audio;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioFormat;
import android.media.AudioManager;
/**
* Notifies a listener when the audio playback capabilities change. Call {@link #register} to start
* receiving notifications, and {@link #unregister} to stop.
*/
public final class AudioCapabilitiesReceiver {
/** Listener notified when audio capabilities change. */
public interface Listener {
/** Called when the audio capabilities change. */
void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities);
}
/** Default to stereo PCM on SDK < 21 and when HDMI is unplugged. */
private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);
private final Context context;
private final Listener listener;
private final BroadcastReceiver receiver;
/**
* Constructs a new audio capabilities receiver.
*
* @param context Application context for registering to receive broadcasts.
* @param listener Listener to notify when audio capabilities change.
*/
public AudioCapabilitiesReceiver(Context context, Listener listener) {
this.context = Assertions.checkNotNull(context);
this.listener = Assertions.checkNotNull(listener);
this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null;
}
/**
* Registers to notify the listener when audio capabilities change. The listener will immediately
* receive the current audio capabilities. It is important to call {@link #unregister} so that
* the listener can be garbage collected.
*/
@TargetApi(21)
public void register() {
if (receiver != null) {
context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
}
listener.onAudioCapabilitiesChanged(DEFAULT_AUDIO_CAPABILITIES);
}
/** Unregisters to stop notifying the listener when audio capabilities change. */
public void unregister() {
if (receiver != null) {
context.unregisterReceiver(receiver);
}
}
@TargetApi(21)
private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (!action.equals(AudioManager.ACTION_HDMI_AUDIO_PLUG)) {
return;
}
listener.onAudioCapabilitiesChanged(
new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0)));
}
}
}
......@@ -135,18 +135,6 @@ public abstract class Chunk implements Loadable {
}
/**
* Returns a byte array containing the loaded data. If the chunk is partially loaded, this
* method returns the data that has been loaded so far. If nothing has been loaded, null is
* returned.
*
* @return The loaded data or null.
*/
public final byte[] getLoadedData() {
Assertions.checkState(dataSourceStream != null);
return dataSourceStream.getLoadedData();
}
/**
* Invoked by {@link #consume()}. Implementations may override this method if they wish to
* consume the loaded data at this point.
* <p>
......
......@@ -45,6 +45,8 @@ public interface ChunkSource {
* 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.
*/
void getMaxVideoDimensions(MediaFormat out);
......
......@@ -41,6 +41,17 @@ public final class Mp4MediaChunk extends MediaChunk {
private Map<UUID, byte[]> psshInfo;
/**
* @deprecated Use the other constructor, passing null as {@code psshInfo}.
*/
@Deprecated
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
extractor, null, maybeSelfContained, sampleOffsetUs);
}
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
......@@ -49,6 +60,8 @@ public final class Mp4MediaChunk extends MediaChunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param extractor The extractor that will be used to extract the samples.
* @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it
* can be obtained directly from {@code extractor}, or if no pssh data is required.
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
* contain a moov atom defining the media format of the chunk. This parameter can always be
* safely set to true. Setting to false where the chunk is known to not be self contained may
......@@ -56,12 +69,13 @@ public final class Mp4MediaChunk extends MediaChunk {
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
*/
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor,
Map<UUID, byte[]> psshInfo, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs;
this.psshInfo = psshInfo;
}
@Override
......@@ -96,8 +110,11 @@ public final class Mp4MediaChunk extends MediaChunk {
prepared = true;
}
if (prepared) {
mediaFormat = Assertions.checkNotNull(extractor.getFormat());
psshInfo = extractor.getPsshInfo();
mediaFormat = extractor.getFormat();
Map<UUID, byte[]> extractorPsshInfo = extractor.getPsshInfo();
if (extractorPsshInfo != null) {
psshInfo = extractorPsshInfo;
}
}
}
return prepared;
......
......@@ -46,6 +46,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
this.selectedSource = sources[0];
}
public MultiTrackChunkSource(List<ChunkSource> sources) {
this(toChunkSourceArray(sources));
}
/**
* Gets the number of tracks that this source can switch between. May be called safely from any
* thread.
......@@ -107,4 +111,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
selectedSource.onChunkLoadError(chunk, e);
}
private static ChunkSource[] toChunkSourceArray(List<ChunkSource> sources) {
ChunkSource[] chunkSourceArray = new ChunkSource[sources.size()];
sources.toArray(chunkSourceArray);
return chunkSourceArray;
}
}
/*
* 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.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import java.io.IOException;
import java.util.List;
/**
* A chunk source that provides a single chunk containing a single sample.
* <p>
* An example use case for this implementation is to act as the source for loading out-of-band
* subtitles, where subtitles for the entire video are delivered as a single file.
*/
public class SingleSampleChunkSource implements ChunkSource {
private final DataSource dataSource;
private final DataSpec dataSpec;
private final Format format;
private final long durationUs;
private final MediaFormat mediaFormat;
private final TrackInfo trackInfo;
/**
* @param dataSource A {@link DataSource} suitable for loading the sample data.
* @param dataSpec Defines the location of the sample.
* @param format The format of the sample.
* @param durationUs The duration of the sample in microseconds, or {@link C#UNKNOWN_TIME_US} if
* the duration is unknown.
* @param mediaFormat The sample media format. May be null.
*/
public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format,
long durationUs, MediaFormat mediaFormat) {
this.dataSource = dataSource;
this.dataSpec = dataSpec;
this.format = format;
this.durationUs = durationUs;
this.mediaFormat = mediaFormat;
trackInfo = new TrackInfo(format.mimeType, durationUs);
}
@Override
public TrackInfo getTrackInfo() {
return trackInfo;
}
@Override
public void getMaxVideoDimensions(MediaFormat out) {
// Do nothing.
}
@Override
public void enable() {
// Do nothing.
}
@Override
public void continueBuffering(long playbackPositionUs) {
// Do nothing.
}
@Override
public void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
long playbackPositionUs, ChunkOperationHolder out) {
if (!queue.isEmpty()) {
// We've already provided the single sample.
return;
}
out.chunk = initChunk();
}
@Override
public void disable(List<? extends MediaChunk> queue) {
// Do nothing.
}
@Override
public IOException getError() {
return null;
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private SingleSampleMediaChunk initChunk() {
return new SingleSampleMediaChunk(dataSource, dataSpec, format, 0, 0, durationUs, -1,
mediaFormat);
}
}
......@@ -22,7 +22,6 @@ import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.UUID;
......@@ -64,10 +63,8 @@ public class SingleSampleMediaChunk extends MediaChunk {
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param sampleFormat The format of the media contained by the chunk.
* @param headerData Custom header data for the sample. May be null. If set, the header data is
* prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is
* however not considered part of the loaded data, and so is not prepended to the data
* returned by {@link #getLoadedData()}. It is also not reflected in the values returned by
* {@link #bytesLoaded()} and {@link #getLength()}.
* prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is not
* reflected in the values returned by {@link #bytesLoaded()} and {@link #getLength()}.
*/
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat,
......@@ -99,9 +96,8 @@ public class SingleSampleMediaChunk extends MediaChunk {
if (headerData != null) {
sampleSize += headerData.length;
}
if (holder.allowDataBufferReplacement &&
(holder.data == null || holder.data.capacity() < sampleSize)) {
holder.data = ByteBuffer.allocate(sampleSize);
if (holder.data == null || holder.data.capacity() < sampleSize) {
holder.replaceBuffer(sampleSize);
}
int bytesRead;
if (holder.data != null) {
......
......@@ -21,7 +21,7 @@ import java.util.List;
/**
* Represents a set of interchangeable encoded versions of a media content component.
*/
public final class AdaptationSet {
public class AdaptationSet {
public static final int TYPE_UNKNOWN = -1;
public static final int TYPE_VIDEO = 0;
......
......@@ -15,36 +15,37 @@
*/
package com.google.android.exoplayer.dash.mpd;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
/**
* Represents a ContentProtection tag in an AdaptationSet. Holds arbitrary data for various DRM
* schemes.
* Represents a ContentProtection tag in an AdaptationSet.
*/
public final class ContentProtection {
public class ContentProtection {
/**
* Identifies the content protection scheme.
*/
public final String schemeUriId;
/**
* Protection scheme specific data.
* The UUID of the protection scheme. May be null.
*/
public final Map<String, String> keyedData;
public final UUID uuid;
/**
* Protection scheme specific data. May be null.
*/
public final byte[] data;
/**
* @param schemeUriId Identifies the content protection scheme.
* @param keyedData Data specific to the scheme.
* @param uuid The UUID of the protection scheme, if known. May be null.
* @param data Protection scheme specific initialization data. May be null.
*/
public ContentProtection(String schemeUriId, Map<String, String> keyedData) {
public ContentProtection(String schemeUriId, UUID uuid, byte[] data) {
this.schemeUriId = schemeUriId;
if (keyedData != null) {
this.keyedData = Collections.unmodifiableMap(keyedData);
} else {
this.keyedData = Collections.emptyMap();
}
this.uuid = uuid;
this.data = data;
}
}
......@@ -21,7 +21,7 @@ import java.util.List;
/**
* Represents a DASH media presentation description (mpd).
*/
public final class MediaPresentationDescription {
public class MediaPresentationDescription {
public final long availabilityStartTime;
......
/*
* 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.dash.mpd;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import java.io.IOException;
import java.io.InputStream;
/**
* A concrete implementation of {@link ManifestFetcher} for loading DASH manifests.
* <p>
* This class is provided for convenience, however it is expected that most applications will
* contain their own mechanisms for making asynchronous network requests and parsing the response.
* In such cases it is recommended that application developers use their existing solution rather
* than this one.
*/
public final class MediaPresentationDescriptionFetcher extends
ManifestFetcher<MediaPresentationDescription> {
private final MediaPresentationDescriptionParser parser;
/**
* @param callback The callback to provide with the parsed manifest (or error).
*/
public MediaPresentationDescriptionFetcher(
ManifestCallback<MediaPresentationDescription> callback) {
super(callback);
parser = new MediaPresentationDescriptionParser();
}
/**
* @param callback The callback to provide with the parsed manifest (or error).
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
*/
public MediaPresentationDescriptionFetcher(
ManifestCallback<MediaPresentationDescription> callback, int timeoutMillis) {
super(callback, timeoutMillis);
parser = new MediaPresentationDescriptionParser();
}
@Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId, Uri baseUrl) throws IOException, ParserException {
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
}
}
......@@ -21,7 +21,7 @@ import java.util.List;
/**
* Encapsulates media content components over a contiguous period of time.
*/
public final class Period {
public class Period {
/**
* The period identifier, if one exists.
......@@ -56,4 +56,21 @@ public final class Period {
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
}
/**
* Returns the index of the first adaptation set of a given type, or -1 if no adaptation set of
* the specified type exists.
*
* @param type An adaptation set type.
* @return The index of the first adaptation set of the specified type, or -1.
*/
public int getAdaptationSetIndex(int type) {
int adaptationCount = adaptationSets.size();
for (int i = 0; i < adaptationCount; i++) {
if (adaptationSets.get(i).type == type) {
return i;
}
}
return -1;
}
}
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
......@@ -47,15 +48,10 @@ public final class RangedUri {
/**
* Constructs an ranged uri.
* <p>
* The uri is built according to the following rules:
* <ul>
* <li>If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is
* ignored and the url consists solely of {@code stringUri}.
* <li>If {@code stringUri} is null, then the url consists solely of {@code baseUrl}.
* <li>Otherwise, the url consists of the concatenation of {@code baseUri} and {@code stringUri}.
* </ul>
* See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and
* {@code stringUri} are merged.
*
* @param baseUri An uri that can form the base of the uri defined by the instance.
* @param baseUri A uri that can form the base of the uri defined by the instance.
* @param stringUri A relative or absolute uri in string form.
* @param start The (zero based) index of the first byte of the range.
* @param length The length of the range, or -1 to indicate that the range is unbounded.
......@@ -74,14 +70,7 @@ public final class RangedUri {
* @return The {@link Uri} represented by the instance.
*/
public Uri getUri() {
if (stringUri == null) {
return baseUri;
}
Uri uri = Uri.parse(stringUri);
if (!uri.isAbsolute() && baseUri != null) {
uri = Uri.withAppendedPath(baseUri, stringUri);
}
return uri;
return Util.getMergedUri(baseUri, stringUri);
}
/**
......
......@@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import java.util.List;
......@@ -139,11 +142,12 @@ public abstract class SegmentBase {
public final long getSegmentDurationUs(int sequenceNumber) {
if (segmentTimeline != null) {
return (segmentTimeline.get(sequenceNumber - startNumber).duration * 1000000) / timescale;
long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
return (duration * C.MICROS_PER_SECOND) / timescale;
} else {
return sequenceNumber == getLastSegmentNum()
? (periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber)
: ((duration * 1000000L) / timescale);
? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber))
: ((duration * C.MICROS_PER_SECOND) / timescale);
}
}
......@@ -155,7 +159,7 @@ public abstract class SegmentBase {
} else {
unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
}
return (unscaledSegmentTime * 1000000) / timescale;
return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale);
}
public abstract RangedUri getSegmentUrl(Representation representation, int index);
......
......@@ -30,6 +30,7 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
......@@ -61,6 +62,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private final Handler eventHandler;
private final EventListener eventListener;
private final MediaDrm mediaDrm;
private final HashMap<String, String> optionalKeyRequestParameters;
/* package */ final MediaDrmHandler mediaDrmHandler;
/* package */ final MediaDrmCallback callback;
......@@ -71,6 +73,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private Handler postRequestHandler;
private int openCount;
private boolean provisioningInProgress;
private int state;
private MediaCrypto mediaCrypto;
private Exception lastException;
......@@ -79,19 +82,32 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private byte[] sessionId;
/**
* @deprecated Use the other constructor, passing null as {@code optionalKeyRequestParameters}.
*/
@Deprecated
public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
this(uuid, playbackLooper, callback, null, eventHandler, eventListener);
}
/**
* @param uuid The UUID of the drm scheme.
* @param playbackLooper The looper associated with the media playback thread. Should usually be
* obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @throws UnsupportedSchemeException If the specified DRM scheme is not supported.
*/
public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener) throws UnsupportedSchemeException {
this.uuid = uuid;
this.callback = callback;
this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mediaDrm = new MediaDrm(uuid);
......@@ -179,6 +195,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
return;
}
state = STATE_CLOSED;
provisioningInProgress = false;
mediaDrmHandler.removeCallbacksAndMessages(null);
postResponseHandler.removeCallbacksAndMessages(null);
postRequestHandler.removeCallbacksAndMessages(null);
......@@ -212,11 +229,16 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
}
private void postProvisionRequest() {
if (provisioningInProgress) {
return;
}
provisioningInProgress = true;
ProvisionRequest request = mediaDrm.getProvisionRequest();
postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
}
private void onProvisionResponse(Object response) {
provisioningInProgress = false;
if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
......@@ -243,7 +265,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
KeyRequest keyRequest;
try {
keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType,
MediaDrm.KEY_TYPE_STREAMING, null);
MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) {
onKeysError(e);
......@@ -277,13 +299,13 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
}
}
private void onError(Exception e) {
private void onError(final Exception e) {
lastException = e;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDrmSessionManagerError(lastException);
eventListener.onDrmSessionManagerError(e);
}
});
}
......
......@@ -24,6 +24,10 @@ import java.util.ArrayList;
public static final int TYPE_esds = 0x65736473;
public static final int TYPE_mdat = 0x6D646174;
public static final int TYPE_mp4a = 0x6D703461;
public static final int TYPE_ac_3 = 0x61632D33; // ac-3
public static final int TYPE_dac3 = 0x64616333;
public static final int TYPE_ec_3 = 0x65632D33; // ec-3
public static final int TYPE_dec3 = 0x64656333;
public static final int TYPE_tfdt = 0x74666474;
public static final int TYPE_tfhd = 0x74666864;
public static final int TYPE_trex = 0x74726578;
......@@ -53,6 +57,7 @@ import java.util.ArrayList;
public static final int TYPE_saiz = 0x7361697A;
public static final int TYPE_uuid = 0x75756964;
public static final int TYPE_senc = 0x73656E63;
public static final int TYPE_pasp = 0x70617370;
public final int type;
......
......@@ -33,14 +33,14 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
*/
public int[] sampleSizeTable;
/**
* The decoding time of each sample in the run.
*/
public int[] sampleDecodingTimeTable;
/**
* The composition time offset of each sample in the run.
*/
public int[] sampleCompositionTimeOffsetTable;
/**
* The decoding time of each sample in the run.
*/
public long[] sampleDecodingTimeTable;
/**
* Indicates which samples are sync frames.
*/
public boolean[] sampleIsSyncFrameTable;
......@@ -95,8 +95,8 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
// likely. The choice of 25% is relatively arbitrary.
int tableSize = (sampleCount * 125) / 100;
sampleSizeTable = new int[tableSize];
sampleDecodingTimeTable = new int[tableSize];
sampleCompositionTimeOffsetTable = new int[tableSize];
sampleDecodingTimeTable = new long[tableSize];
sampleIsSyncFrameTable = new boolean[tableSize];
sampleHasSubsampleEncryptionTable = new boolean[tableSize];
}
......@@ -147,7 +147,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
return true;
}
public int getSamplePresentationTime(int index) {
public long getSamplePresentationTime(int index) {
return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
}
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
......@@ -133,7 +135,7 @@ import java.util.Stack;
}
@Override
public int read(NonBlockingInputStream inputStream) {
public int read(NonBlockingInputStream inputStream) throws ParserException {
Assertions.checkState(eventHandler != null);
while (true) {
while (!masterElementsStack.isEmpty()
......@@ -210,7 +212,7 @@ import java.util.Stack;
if (stringResult != READ_RESULT_CONTINUE) {
return stringResult;
}
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
String stringValue = new String(stringBytes, Charset.forName(C.UTF8_NAME));
stringBytes = null;
eventHandler.onStringElement(elementId, stringValue);
prepareForNextElement();
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.nio.ByteBuffer;
......@@ -46,41 +47,47 @@ import java.nio.ByteBuffer;
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's children
* @throws ParserException If a parsing error occurs.
*/
public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
int id, long elementOffsetBytes, int headerSizeBytes,
long contentsSizeBytes) throws ParserException;
/**
* Called when a master element has finished reading in all of its children from the
* {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @throws ParserException If a parsing error occurs.
*/
public void onMasterElementEnd(int id);
public void onMasterElementEnd(int id) throws ParserException;
/**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The integer value this element contains
* @throws ParserException If a parsing error occurs.
*/
public void onIntegerElement(int id, long value);
public void onIntegerElement(int id, long value) throws ParserException;
/**
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The float value this element contains
* @throws ParserException If a parsing error occurs.
*/
public void onFloatElement(int id, double value);
public void onFloatElement(int id, double value) throws ParserException;
/**
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The string value this element contains
* @throws ParserException If a parsing error occurs.
*/
public void onStringElement(int id, String value);
public void onStringElement(int id, String value) throws ParserException;
/**
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
......@@ -109,9 +116,10 @@ import java.nio.ByteBuffer;
* @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read
* @return True if the element was read. False otherwise.
* @throws ParserException If a parsing error occurs.
*/
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream);
NonBlockingInputStream inputStream) throws ParserException;
}
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.nio.ByteBuffer;
......@@ -53,8 +54,9 @@ import java.nio.ByteBuffer;
*
* @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this interface
* @throws ParserException If parsing fails.
*/
public int read(NonBlockingInputStream inputStream);
public int read(NonBlockingInputStream inputStream) throws ParserException;
/**
* The total number of bytes consumed by the reader since first created or last {@link #reset()}.
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import java.io.IOException;
import java.io.InputStream;
/**
* A concrete implementation of {@link ManifestFetcher} for loading SmoothStreaming
* manifests.
* <p>
* This class is provided for convenience, however it is expected that most applications will
* contain their own mechanisms for making asynchronous network requests and parsing the response.
* In such cases it is recommended that application developers use their existing solution rather
* than this one.
*/
public final class SmoothStreamingManifestFetcher extends ManifestFetcher<SmoothStreamingManifest> {
private final SmoothStreamingManifestParser parser;
/**
* @param callback The callback to provide with the parsed manifest (or error).
*/
public SmoothStreamingManifestFetcher(ManifestCallback<SmoothStreamingManifest> callback) {
super(callback);
parser = new SmoothStreamingManifestParser();
}
/**
* @param callback The callback to provide with the parsed manifest (or error).
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
*/
public SmoothStreamingManifestFetcher(ManifestCallback<SmoothStreamingManifest> callback,
int timeoutMillis) {
super(callback, timeoutMillis);
parser = new SmoothStreamingManifestParser();
}
@Override
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
String contentId, Uri baseUrl) throws IOException, ParserException {
return parser.parse(stream, inputEncoding);
}
}
/*
* 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;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.graphics.Color;
import android.graphics.Typeface;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptionStyle;
/**
* A compatibility wrapper for {@link CaptionStyle}.
*/
public final class CaptionStyleCompat {
/**
* Edge type value specifying no character edges.
*/
public static final int EDGE_TYPE_NONE = 0;
/**
* Edge type value specifying uniformly outlined character edges.
*/
public static final int EDGE_TYPE_OUTLINE = 1;
/**
* Edge type value specifying drop-shadowed character edges.
*/
public static final int EDGE_TYPE_DROP_SHADOW = 2;
/**
* Edge type value specifying raised bevel character edges.
*/
public static final int EDGE_TYPE_RAISED = 3;
/**
* Edge type value specifying depressed bevel character edges.
*/
public static final int EDGE_TYPE_DEPRESSED = 4;
/**
* Use color setting specified by the track and fallback to default caption style.
*/
public static final int USE_TRACK_COLOR_SETTINGS = 1;
/**
* Default caption style.
*/
public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat(
Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null);
/**
* The preferred foreground color.
*/
public final int foregroundColor;
/**
* The preferred background color.
*/
public final int backgroundColor;
/**
* The preferred window color.
*/
public final int windowColor;
/**
* The preferred edge type. One of:
* <ul>
* <li>{@link #EDGE_TYPE_NONE}
* <li>{@link #EDGE_TYPE_OUTLINE}
* <li>{@link #EDGE_TYPE_DROP_SHADOW}
* <li>{@link #EDGE_TYPE_RAISED}
* <li>{@link #EDGE_TYPE_DEPRESSED}
* </ul>
*/
public final int edgeType;
/**
* The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}.
*/
public final int edgeColor;
/**
* The preferred typeface.
*/
public final Typeface typeface;
/**
* Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}.
*
* @param captionStyle A {@link CaptionStyle}.
* @return The equivalent {@link CaptionStyleCompat}.
*/
@TargetApi(19)
public static CaptionStyleCompat createFromCaptionStyle(
CaptioningManager.CaptionStyle captionStyle) {
if (Util.SDK_INT >= 21) {
return createFromCaptionStyleV21(captionStyle);
} else {
// Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did
// not exist in earlier API levels).
return createFromCaptionStyleV19(captionStyle);
}
}
/**
* @param foregroundColor See {@link #foregroundColor}.
* @param backgroundColor See {@link #backgroundColor}.
* @param windowColor See {@link #windowColor}.
* @param edgeType See {@link #edgeType}.
* @param edgeColor See {@link #edgeColor}.
* @param typeface See {@link #typeface}.
*/
public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, int edgeType,
int edgeColor, Typeface typeface) {
this.foregroundColor = foregroundColor;
this.backgroundColor = backgroundColor;
this.windowColor = windowColor;
this.edgeType = edgeType;
this.edgeColor = edgeColor;
this.typeface = typeface;
}
@TargetApi(19)
private static CaptionStyleCompat createFromCaptionStyleV19(
CaptioningManager.CaptionStyle captionStyle) {
return new CaptionStyleCompat(
captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT,
captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface());
}
@TargetApi(21)
private static CaptionStyleCompat createFromCaptionStyleV21(
CaptioningManager.CaptionStyle captionStyle) {
return new CaptionStyleCompat(
captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor,
captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor,
captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor,
captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType,
captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor,
captionStyle.getTypeface());
}
}
/*
* 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;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.Assertions;
import android.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Wraps a {@link SubtitleParser}, exposing an interface similar to {@link MediaCodec} for
* asynchronous parsing of subtitles.
*/
public class SubtitleParserHelper implements Handler.Callback {
private final SubtitleParser parser;
private final Handler handler;
private SampleHolder sampleHolder;
private boolean parsing;
private Subtitle result;
private IOException error;
/**
* @param looper The {@link Looper} associated with the thread on which parsing should occur.
* @param parser The parser that should be used to parse the raw data.
*/
public SubtitleParserHelper(Looper looper, SubtitleParser parser) {
this.handler = new Handler(looper, this);
this.parser = parser;
flush();
}
/**
* Flushes the helper, canceling the current parsing operation, if there is one.
*/
public synchronized void flush() {
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
parsing = false;
result = null;
error = null;
}
/**
* Whether the helper is currently performing a parsing operation.
*
* @return True if the helper is currently performing a parsing operation. False otherwise.
*/
public synchronized boolean isParsing() {
return parsing;
}
/**
* Gets the holder that should be populated with data to be parsed.
* <p>
* The returned holder will remain valid unless {@link #flush()} is called. If {@link #flush()}
* is called the holder is replaced, and this method should be called again to obtain the new
* holder.
*
* @return The holder that should be populated with data to be parsed.
*/
public synchronized SampleHolder getSampleHolder() {
return sampleHolder;
}
/**
* Start a parsing operation.
* <p>
* The holder returned by {@link #getSampleHolder()} should be populated with the data to be
* parsed prior to calling this method.
*/
public synchronized void startParseOperation() {
Assertions.checkState(!parsing);
parsing = true;
result = null;
error = null;
handler.obtainMessage(0, sampleHolder).sendToTarget();
}
/**
* Gets the result of the most recent parsing operation.
* <p>
* The result is cleared as a result of calling this method, and so subsequent calls will return
* null until a subsequent parsing operation has finished.
*
* @return The result of the parsing operation, or null.
* @throws IOException If the parsing operation failed.
*/
public synchronized Subtitle getAndClearResult() throws IOException {
try {
if (error != null) {
throw error;
}
return result;
} finally {
error = null;
result = null;
}
}
@Override
public boolean handleMessage(Message msg) {
Subtitle result;
IOException error;
SampleHolder holder = (SampleHolder) msg.obj;
try {
InputStream inputStream = new ByteArrayInputStream(holder.data.array(), 0, holder.size);
result = parser.parse(inputStream, null, sampleHolder.timeUs);
error = null;
} catch (IOException e) {
result = null;
error = e;
}
synchronized (this) {
if (sampleHolder != holder) {
// A flush has occurred since this holder was posted. Do nothing.
} else {
holder.data.position(0);
this.result = result;
this.error = error;
this.parsing = false;
}
}
return true;
}
}
/*
* 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;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
/**
* A view for rendering captions.
* <p>
* The caption style and text size can be configured using {@link #setStyle(CaptionStyleCompat)} and
* {@link #setTextSize(float)} respectively.
*/
public class SubtitleView extends View {
/**
* Ratio of inner padding to font size.
*/
private static final float INNER_PADDING_RATIO = 0.125f;
/**
* Temporary rectangle used for computing line bounds.
*/
private final RectF lineBounds = new RectF();
/**
* Reusable string builder used for holding text.
*/
private final StringBuilder textBuilder = new StringBuilder();
// Styled dimensions.
private final float cornerRadius;
private final float outlineWidth;
private final float shadowRadius;
private final float shadowOffset;
private TextPaint textPaint;
private Paint paint;
private int foregroundColor;
private int backgroundColor;
private int edgeColor;
private int edgeType;
private boolean hasMeasurements;
private int lastMeasuredWidth;
private StaticLayout layout;
private float spacingMult;
private float spacingAdd;
private int innerPaddingX;
public SubtitleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
int[] viewAttr = {android.R.attr.text, android.R.attr.textSize,
android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier};
TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0);
CharSequence text = a.getText(0);
int textSize = a.getDimensionPixelSize(1, 15);
spacingAdd = a.getDimensionPixelSize(2, 0);
spacingMult = a.getFloat(3, 1);
a.recycle();
Resources resources = getContext().getResources();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
int twoDpInPx = Math.round((2 * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
cornerRadius = twoDpInPx;
outlineWidth = twoDpInPx;
shadowRadius = twoDpInPx;
shadowOffset = twoDpInPx;
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setSubpixelText(true);
paint = new Paint();
paint.setAntiAlias(true);
innerPaddingX = 0;
setText(text);
setTextSize(textSize);
setStyle(CaptionStyleCompat.DEFAULT);
}
public SubtitleView(Context context) {
this(context, null);
}
@Override
public void setBackgroundColor(int color) {
backgroundColor = color;
invalidate();
}
/**
* Sets the text to be displayed by the view.
*
* @param text The text to display.
*/
public void setText(CharSequence text) {
textBuilder.setLength(0);
textBuilder.append(text);
hasMeasurements = false;
requestLayout();
}
/**
* Sets the text size in pixels.
*
* @param size The text size in pixels.
*/
public void setTextSize(float size) {
if (textPaint.getTextSize() != size) {
textPaint.setTextSize(size);
innerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
hasMeasurements = false;
requestLayout();
invalidate();
}
}
/**
* Configures the view according to the given style.
*
* @param style A style for the view.
*/
public void setStyle(CaptionStyleCompat style) {
foregroundColor = style.foregroundColor;
backgroundColor = style.backgroundColor;
edgeType = style.edgeType;
edgeColor = style.edgeColor;
setTypeface(style.typeface);
super.setBackgroundColor(style.windowColor);
hasMeasurements = false;
requestLayout();
}
private void setTypeface(Typeface typeface) {
if (textPaint.getTypeface() != typeface) {
textPaint.setTypeface(typeface);
hasMeasurements = false;
requestLayout();
invalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
if (computeMeasurements(widthSpec)) {
final StaticLayout layout = this.layout;
final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2;
final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom();
int width = 0;
int lineCount = layout.getLineCount();
for (int i = 0; i < lineCount; i++) {
width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width);
}
width += paddingX;
setMeasuredDimension(width, height);
} else if (Util.SDK_INT >= 11) {
setTooSmallMeasureDimensionV11();
} else {
setMeasuredDimension(0, 0);
}
}
@TargetApi(11)
private void setTooSmallMeasureDimensionV11() {
setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
}
@Override
public void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
computeMeasurements(width);
}
private boolean computeMeasurements(int maxWidth) {
if (hasMeasurements && maxWidth == lastMeasuredWidth) {
return true;
}
// Account for padding.
final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2;
maxWidth -= paddingX;
if (maxWidth <= 0) {
return false;
}
hasMeasurements = true;
lastMeasuredWidth = maxWidth;
layout = new StaticLayout(textBuilder, textPaint, maxWidth, null, spacingMult, spacingAdd,
true);
return true;
}
@Override
protected void onDraw(Canvas c) {
final StaticLayout layout = this.layout;
if (layout == null) {
return;
}
final int saveCount = c.save();
final int innerPaddingX = this.innerPaddingX;
c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop());
final int lineCount = layout.getLineCount();
final Paint textPaint = this.textPaint;
final Paint paint = this.paint;
final RectF bounds = lineBounds;
if (Color.alpha(backgroundColor) > 0) {
final float cornerRadius = this.cornerRadius;
float previousBottom = layout.getLineTop(0);
paint.setColor(backgroundColor);
paint.setStyle(Style.FILL);
for (int i = 0; i < lineCount; i++) {
bounds.left = layout.getLineLeft(i) - innerPaddingX;
bounds.right = layout.getLineRight(i) + innerPaddingX;
bounds.top = previousBottom;
bounds.bottom = layout.getLineBottom(i);
previousBottom = bounds.bottom;
c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
}
}
if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
textPaint.setStrokeJoin(Join.ROUND);
textPaint.setStrokeWidth(outlineWidth);
textPaint.setColor(edgeColor);
textPaint.setStyle(Style.FILL_AND_STROKE);
layout.draw(c);
} else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {
textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor);
} else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED
|| edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {
boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;
int colorUp = raised ? Color.WHITE : edgeColor;
int colorDown = raised ? edgeColor : Color.WHITE;
float offset = shadowRadius / 2f;
textPaint.setColor(foregroundColor);
textPaint.setStyle(Style.FILL);
textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp);
layout.draw(c);
textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown);
}
textPaint.setColor(foregroundColor);
textPaint.setStyle(Style.FILL);
layout.draw(c);
textPaint.setShadowLayer(0, 0, 0, 0);
c.restoreToCount(saveCount);
}
}
......@@ -20,20 +20,16 @@ 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.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.VerboseLogUtil;
import android.annotation.TargetApi;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a
......@@ -56,14 +52,11 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
}
private static final String TAG = "TextTrackRenderer";
private static final int MSG_UPDATE_OVERLAY = 0;
private final Handler textRendererHandler;
private final TextRenderer textRenderer;
private final SampleSource source;
private final SampleHolder sampleHolder;
private final MediaFormatHolder formatHolder;
private final SubtitleParser subtitleParser;
......@@ -73,6 +66,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
private boolean inputStreamEnded;
private Subtitle subtitle;
private SubtitleParserHelper parserHelper;
private HandlerThread parserThread;
private int nextSubtitleEventIndex;
private boolean textRendererNeedsUpdate;
......@@ -94,7 +89,6 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
this);
formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(true);
}
@Override
......@@ -117,46 +111,66 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
}
@Override
protected void onEnabled(long timeUs, boolean joining) {
source.enable(trackIndex, timeUs);
seekToInternal(timeUs);
protected void onEnabled(long positionUs, boolean joining) {
source.enable(trackIndex, positionUs);
parserThread = new HandlerThread("textParser");
parserThread.start();
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser);
seekToInternal(positionUs);
}
@Override
protected void seekTo(long timeUs) {
source.seekToUs(timeUs);
seekToInternal(timeUs);
protected void seekTo(long positionUs) {
source.seekToUs(positionUs);
seekToInternal(positionUs);
}
private void seekToInternal(long timeUs) {
private void seekToInternal(long positionUs) {
inputStreamEnded = false;
currentPositionUs = timeUs;
source.seekToUs(timeUs);
if (subtitle != null && (timeUs < subtitle.getStartTime()
|| subtitle.getLastEventTime() <= timeUs)) {
currentPositionUs = positionUs;
source.seekToUs(positionUs);
if (subtitle != null && (positionUs < subtitle.getStartTime()
|| subtitle.getLastEventTime() <= positionUs)) {
subtitle = null;
}
resetSampleData();
parserHelper.flush();
clearTextRenderer();
syncNextEventIndex(timeUs);
syncNextEventIndex(positionUs);
textRendererNeedsUpdate = subtitle != null;
}
@Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
currentPositionUs = positionUs;
try {
source.continueBuffering(timeUs);
source.continueBuffering(positionUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
currentPositionUs = timeUs;
if (parserHelper.isParsing()) {
return;
}
Subtitle dequeuedSubtitle = null;
if (subtitle == null) {
try {
dequeuedSubtitle = parserHelper.getAndClearResult();
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance
// to the next event.
if (subtitle != null) {
if (subtitle == null && dequeuedSubtitle != null) {
// We've dequeued a new subtitle. Sync the event index and update the subtitle.
subtitle = dequeuedSubtitle;
syncNextEventIndex(positionUs);
textRendererNeedsUpdate = true;
} else if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
long nextEventTimeUs = getNextEventTime();
while (nextEventTimeUs <= timeUs) {
while (nextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++;
nextEventTimeUs = getNextEventTime();
textRendererNeedsUpdate = true;
......@@ -169,27 +183,17 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
// We don't have a subtitle. Try and read the next one from the source, and if we succeed then
// sync and set textRendererNeedsUpdate.
if (subtitle == null) {
boolean resetSampleHolder = false;
if (!inputStreamEnded && subtitle == null) {
try {
int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false);
SampleHolder sampleHolder = parserHelper.getSampleHolder();
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
resetSampleHolder = true;
InputStream subtitleInputStream =
new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size);
subtitle = subtitleParser.parse(subtitleInputStream, "UTF-8", sampleHolder.timeUs);
syncNextEventIndex(timeUs);
textRendererNeedsUpdate = true;
parserHelper.startParseOperation();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}
} catch (IOException e) {
resetSampleHolder = true;
throw new ExoPlaybackException(e);
} finally {
if (resetSampleHolder) {
resetSampleData();
}
}
}
......@@ -199,17 +203,19 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
if (subtitle == null) {
clearTextRenderer();
} else {
updateTextRenderer(timeUs);
updateTextRenderer(positionUs);
}
}
}
@Override
protected void onDisabled() {
source.disable(trackIndex);
subtitle = null;
resetSampleData();
parserThread.quit();
parserThread = null;
parserHelper = null;
clearTextRenderer();
source.disable(trackIndex);
}
@Override
......@@ -241,12 +247,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
@Override
protected boolean isReady() {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [redacted].
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true;
}
private void syncNextEventIndex(long timeUs) {
nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(timeUs);
private void syncNextEventIndex(long positionUs) {
nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(positionUs);
}
private long getNextEventTime() {
......@@ -255,28 +261,20 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
: (subtitle.getEventTime(nextSubtitleEventIndex));
}
private void resetSampleData() {
if (sampleHolder.data != null) {
sampleHolder.data.position(0);
}
}
private void updateTextRenderer(long timeUs) {
String text = subtitle.getText(timeUs);
log("updateTextRenderer; text=: " + text);
private void updateTextRenderer(long positionUs) {
String text = subtitle.getText(positionUs);
if (textRendererHandler != null) {
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget();
} else {
invokeTextRenderer(text);
invokeRendererInternal(text);
}
}
private void clearTextRenderer() {
log("clearTextRenderer");
if (textRendererHandler != null) {
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget();
} else {
invokeTextRenderer(null);
invokeRendererInternal(null);
}
}
......@@ -284,20 +282,14 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_OVERLAY:
invokeTextRenderer((String) msg.obj);
invokeRendererInternal((String) msg.obj);
return true;
}
return false;
}
private void invokeTextRenderer(String text) {
private void invokeRendererInternal(String text) {
textRenderer.onText(text);
}
private void log(String logMessage) {
if (VerboseLogUtil.isTagEnabled(TAG)) {
Log.v(TAG, "type=" + AdaptationSet.TYPE_TEXT + ", " + logMessage);
}
}
}
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes;
......@@ -72,8 +74,23 @@ public class TtmlParser implements SubtitleParser {
private static final int DEFAULT_TICKRATE = 1;
private final XmlPullParserFactory xmlParserFactory;
private final boolean strictParsing;
/**
* Equivalent to {@code TtmlParser(true)}.
*/
public TtmlParser() {
this(true);
}
/**
* @param strictParsing If true, {@link #parse(InputStream, String, long)} will throw a
* {@link ParserException} if the stream contains invalid ttml. If false, the parser will
* make a best effort to ignore minor errors in the stream. Note however that a
* {@link ParserException} will still be thrown when this is not possible.
*/
public TtmlParser(boolean strictParsing) {
this.strictParsing = strictParsing;
try {
xmlParserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
......@@ -89,21 +106,31 @@ public class TtmlParser implements SubtitleParser {
xmlParser.setInput(inputStream, inputEncoding);
TtmlSubtitle ttmlSubtitle = null;
LinkedList<TtmlNode> nodeStack = new LinkedList<TtmlNode>();
int unsupportedTagDepth = 0;
int unsupportedNodeDepth = 0;
int eventType = xmlParser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peekLast();
if (unsupportedTagDepth == 0) {
if (unsupportedNodeDepth == 0) {
String name = xmlParser.getName();
if (eventType == XmlPullParser.START_TAG) {
if (!isSupportedTag(name)) {
Log.w(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedTagDepth++;
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++;
} else {
TtmlNode node = parseNode(xmlParser, parent);
nodeStack.addLast(node);
if (parent != null) {
parent.addChild(node);
try {
TtmlNode node = parseNode(xmlParser, parent);
nodeStack.addLast(node);
if (parent != null) {
parent.addChild(node);
}
} catch (ParserException e) {
if (strictParsing) {
throw e;
} else {
Log.e(TAG, "Suppressing parser error", e);
// Treat the node (and by extension, all of its children) as unsupported.
unsupportedNodeDepth++;
}
}
}
} else if (eventType == XmlPullParser.TEXT) {
......@@ -116,9 +143,9 @@ public class TtmlParser implements SubtitleParser {
}
} else {
if (eventType == XmlPullParser.START_TAG) {
unsupportedTagDepth++;
unsupportedNodeDepth++;
} else if (eventType == XmlPullParser.END_TAG) {
unsupportedTagDepth--;
unsupportedNodeDepth--;
}
}
xmlParser.next();
......@@ -126,7 +153,7 @@ public class TtmlParser implements SubtitleParser {
}
return ttmlSubtitle;
} catch (XmlPullParserException xppe) {
throw new IOException("Unable to parse source", xppe);
throw new ParserException("Unable to parse source", xppe);
}
}
......@@ -135,7 +162,7 @@ public class TtmlParser implements SubtitleParser {
return MimeTypes.APPLICATION_TTML.equals(mimeType);
}
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) {
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException {
long duration = 0;
long startTime = TtmlNode.UNDEFINED_TIME;
long endTime = TtmlNode.UNDEFINED_TIME;
......@@ -209,10 +236,10 @@ public class TtmlParser implements SubtitleParser {
* @param subframeRate The sub-framerate of the stream
* @param tickRate The tick rate of the stream.
* @return The parsed timestamp in microseconds.
* @throws NumberFormatException If the given string does not contain a valid time expression.
* @throws ParserException If the given string does not contain a valid time expression.
*/
private static long parseTimeExpression(String time, int frameRate, int subframeRate,
int tickRate) {
int tickRate) throws ParserException {
Matcher matcher = CLOCK_TIME.matcher(time);
if (matcher.matches()) {
String hours = matcher.group(1);
......@@ -228,29 +255,29 @@ public class TtmlParser implements SubtitleParser {
String subframes = matcher.group(6);
durationSeconds += (subframes != null) ?
((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0;
return (long) (durationSeconds * 1000000);
return (long) (durationSeconds * C.MICROS_PER_SECOND);
}
matcher = OFFSET_TIME.matcher(time);
if (matcher.matches()) {
String timeValue = matcher.group(1);
double value = Double.parseDouble(timeValue);
double offsetSeconds = Double.parseDouble(timeValue);
String unit = matcher.group(2);
if (unit.equals("h")) {
value *= 3600L * 1000000L;
offsetSeconds *= 3600;
} else if (unit.equals("m")) {
value *= 60 * 1000000;
offsetSeconds *= 60;
} else if (unit.equals("s")) {
value *= 1000000;
// Do nothing.
} else if (unit.equals("ms")) {
value *= 1000;
offsetSeconds /= 1000;
} else if (unit.equals("f")) {
value = value / frameRate * 1000000;
offsetSeconds /= frameRate;
} else if (unit.equals("t")) {
value = value / tickRate * 1000000;
offsetSeconds /= tickRate;
}
return (long) value;
return (long) (offsetSeconds * C.MICROS_PER_SECOND);
}
throw new NumberFormatException("Malformed time expression: " + time);
throw new ParserException("Malformed time expression: " + time);
}
}
/*
* 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.webvtt;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A simple WebVTT parser.
* <p>
* @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
* <p>
*/
public class WebvttParser implements SubtitleParser {
/**
* This parser allows a custom header to be prepended to the WebVTT data, in the form of a text
* line starting with this string.
*
* @hide
*/
public static final String EXO_HEADER = "EXO-HEADER";
/**
* A {@code OFFSET + value} element can be added to the custom header to specify an offset time
* (in microseconds) that should be subtracted from the embedded MPEGTS value.
*
* @hide
*/
public static final String OFFSET = "OFFSET:";
private static final long SAMPLING_RATE = 90;
private static final String WEBVTT_METADATA_HEADER_STRING = "\\S*[:=]\\S*";
private static final Pattern WEBVTT_METADATA_HEADER =
Pattern.compile(WEBVTT_METADATA_HEADER_STRING);
private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}";
private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING);
private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+");
private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+");
private final boolean strictParsing;
public WebvttParser() {
this(true);
}
public WebvttParser(boolean strictParsing) {
this.strictParsing = strictParsing;
}
@Override
public WebvttSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
throws IOException {
ArrayList<WebvttCue> subtitles = new ArrayList<WebvttCue>();
long mediaTimestampUs = startTimeUs;
long mediaTimestampOffsetUs = 0;
BufferedReader webvttData = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME));
String line;
// file should start with "WEBVTT" on the first line or "EXO-HEADER"
line = webvttData.readLine();
if (line == null) {
throw new ParserException("Expected WEBVTT or EXO-HEADER. Got null");
}
if (line.startsWith(EXO_HEADER)) {
// parse the timestamp offset, if present
Matcher matcher = MEDIA_TIMESTAMP_OFFSET.matcher(line);
if (matcher.find()) {
mediaTimestampOffsetUs = Long.parseLong(matcher.group().substring(7));
}
// read the next line, which should now be WEBVTT
line = webvttData.readLine();
if (line == null) {
throw new ParserException("Expected WEBVTT. Got null");
}
}
if (!line.equals("WEBVTT")) {
throw new ParserException("Expected WEBVTT. Got " + line);
}
// parse the remainder of the header
while (true) {
line = webvttData.readLine();
if (line == null) {
// we reached EOF before finishing the header
throw new ParserException("Expected an empty line after webvtt header");
} else if (line.isEmpty()) {
// we've read the newline that separates the header from the body
break;
}
Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line);
if (!matcher.find()) {
handleNoncompliantLine(line);
}
if (line.startsWith("X-TIMESTAMP-MAP")) {
// parse the media timestamp
Matcher timestampMatcher = MEDIA_TIMESTAMP.matcher(line);
if (!timestampMatcher.find()) {
throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line);
} else {
mediaTimestampUs = (Long.parseLong(timestampMatcher.group().substring(7)) * 1000)
/ SAMPLING_RATE - mediaTimestampOffsetUs;
}
mediaTimestampUs = getAdjustedStartTime(mediaTimestampUs);
}
}
// process the cues and text
while ((line = webvttData.readLine()) != null) {
// parse the cue timestamps
Matcher matcher = WEBVTT_TIMESTAMP.matcher(line);
long startTime;
long endTime;
String text = "";
// parse start timestamp
if (!matcher.find()) {
throw new ParserException("Expected cue start time: " + line);
} else {
startTime = parseTimestampUs(matcher.group()) + mediaTimestampUs;
}
// parse end timestamp
if (!matcher.find()) {
throw new ParserException("Expected cue end time: " + line);
} else {
endTime = parseTimestampUs(matcher.group()) + mediaTimestampUs;
}
// parse text
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
text += line.trim() + "\n";
}
WebvttCue cue = new WebvttCue(startTime, endTime, text);
subtitles.add(cue);
}
webvttData.close();
inputStream.close();
// copy WebvttCue data into arrays for WebvttSubtitle constructor
String[] cueText = new String[subtitles.size()];
long[] cueTimesUs = new long[2 * subtitles.size()];
for (int subtitleIndex = 0; subtitleIndex < subtitles.size(); subtitleIndex++) {
int arrayIndex = subtitleIndex * 2;
WebvttCue cue = subtitles.get(subtitleIndex);
cueTimesUs[arrayIndex] = cue.startTime;
cueTimesUs[arrayIndex + 1] = cue.endTime;
cueText[subtitleIndex] = cue.text;
}
WebvttSubtitle subtitle = new WebvttSubtitle(cueText, mediaTimestampUs, cueTimesUs);
return subtitle;
}
@Override
public boolean canParse(String mimeType) {
return MimeTypes.TEXT_VTT.equals(mimeType);
}
protected long getAdjustedStartTime(long startTimeUs) {
return startTimeUs;
}
protected void handleNoncompliantLine(String line) throws ParserException {
if (strictParsing) {
throw new ParserException("Unexpected line: " + line);
}
}
private static long parseTimestampUs(String s) throws NumberFormatException {
if (!s.matches(WEBVTT_TIMESTAMP_STRING)) {
throw new NumberFormatException("has invalid format");
}
String[] parts = s.split("\\.", 2);
long value = 0;
for (String group : parts[0].split(":")) {
value = value * 60 + Long.parseLong(group);
}
return (value * 1000 + Long.parseLong(parts[1])) * 1000;
}
private static class WebvttCue {
public final long startTime;
public final long endTime;
public final String text;
public WebvttCue(long startTime, long endTime, String text) {
this.startTime = startTime;
this.endTime = endTime;
this.text = text;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment