T checkNotNull(T reference, String errorMessage) {
13 | if (reference == null) {
14 | throw new NullPointerException(errorMessage);
15 | }
16 | return reference;
17 | }
18 |
19 | static void checkArgument(boolean expression) {
20 | if (!expression) {
21 | throw new IllegalArgumentException();
22 | }
23 | }
24 |
25 | static void checkArgument(boolean expression, String errorMessage) {
26 | if (!expression) {
27 | throw new IllegalArgumentException(errorMessage);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/ProxyCache.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 | import android.util.Log;
6 |
7 | import java.util.concurrent.atomic.AtomicInteger;
8 |
9 | import static com.danikula.videocache.Preconditions.checkNotNull;
10 | import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
11 |
12 | /**
13 | * Proxy for {@link Source} with caching support ({@link Cache}).
14 | *
15 | * Can be used only for sources with persistent data (that doesn't change with time).
16 | * Method {@link #read(byte[], long, int)} will be blocked while fetching data from source.
17 | * Useful for streaming something with caching e.g. streaming video/audio etc.
18 | *
19 | * @author Alexey Danilov (danikula@gmail.com).
20 | */
21 | public class ProxyCache {
22 |
23 | private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
24 |
25 | private final Source source;
26 | private final Cache cache;
27 | private final Object wc;
28 | private final Handler handler;
29 | private volatile Thread sourceReaderThread;
30 | private volatile boolean stopped;
31 | private final AtomicInteger readSourceErrorsCount;
32 | private CacheListener cacheListener;
33 | private final boolean logEnabled;
34 |
35 | public ProxyCache(Source source, Cache cache, boolean logEnabled) {
36 | this.source = checkNotNull(source);
37 | this.cache = checkNotNull(cache);
38 | this.logEnabled = logEnabled;
39 | this.wc = new Object();
40 | this.handler = new Handler(Looper.getMainLooper());
41 | this.readSourceErrorsCount = new AtomicInteger();
42 | }
43 |
44 | public ProxyCache(Source source, Cache cache) {
45 | this(source, cache, false);
46 | }
47 |
48 | public void setCacheListener(CacheListener cacheListener) {
49 | this.cacheListener = cacheListener;
50 | }
51 |
52 | public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
53 | ProxyCacheUtils.assertBuffer(buffer, offset, length);
54 |
55 | while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
56 | readSourceAsync();
57 | waitForSourceData();
58 | checkIsCacheValid();
59 | checkReadSourceErrorsCount();
60 | }
61 | int read = cache.read(buffer, offset, length);
62 | if (isLogEnabled()) {
63 | Log.d(LOG_TAG, "Read data[" + read + " bytes] from cache with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, read));
64 | }
65 | return read;
66 | }
67 |
68 | private void checkIsCacheValid() throws ProxyCacheException {
69 | int sourceAvailable = source.available();
70 | if (sourceAvailable > 0 && cache.available() > sourceAvailable) {
71 | throw new ProxyCacheException("Unexpected cache: cache [" + cache.available() + " bytes] > source[" + sourceAvailable + " bytes]");
72 | }
73 | }
74 |
75 | private void checkReadSourceErrorsCount() throws ProxyCacheException {
76 | int errorsCount = readSourceErrorsCount.get();
77 | if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
78 | readSourceErrorsCount.set(0);
79 | throw new ProxyCacheException("Error reading source " + errorsCount + " times");
80 | }
81 | }
82 |
83 | public void shutdown() {
84 | try {
85 | stopped = true;
86 | if (sourceReaderThread != null) {
87 | sourceReaderThread.interrupt();
88 | }
89 | cache.close();
90 | } catch (ProxyCacheException e) {
91 | onError(e);
92 | }
93 | }
94 |
95 | private void readSourceAsync() throws ProxyCacheException {
96 |
97 | boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
98 | if (!stopped && !cache.isCompleted() && !readingInProgress) {
99 | sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for ProxyCache");
100 | sourceReaderThread.start();
101 | }
102 | }
103 |
104 | private void waitForSourceData() throws ProxyCacheException {
105 | synchronized (wc) {
106 | try {
107 | wc.wait(1000);
108 | } catch (InterruptedException e) {
109 | throw new ProxyCacheException("Waiting source data is interrupted!", e);
110 | }
111 | }
112 | }
113 |
114 | private void notifyNewCacheDataAvailable(final int cachePercentage) {
115 | handler.post(new Runnable() {
116 | @Override
117 | public void run() {
118 | if (cacheListener != null) {
119 | cacheListener.onCacheDataAvailable(cachePercentage);
120 | }
121 | }
122 | });
123 |
124 | synchronized (wc) {
125 | wc.notifyAll();
126 | }
127 | }
128 |
129 | private void readSource() {
130 | int cachePercentage = 0;
131 | try {
132 | int offset = cache.available();
133 | source.open(offset);
134 | byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
135 | int readBytes;
136 | while ((readBytes = source.read(buffer)) != -1 && !Thread.currentThread().isInterrupted() && !stopped) {
137 | if (isLogEnabled()) {
138 | Log.d(LOG_TAG, "Write data[" + readBytes + " bytes] to cache from source with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
139 | }
140 | cache.append(buffer, readBytes);
141 | offset += readBytes;
142 | cachePercentage = offset * 100 / source.available();
143 |
144 | notifyNewCacheDataAvailable(cachePercentage);
145 | }
146 | if (cache.available() == source.available()) {
147 | cache.complete();
148 | }
149 | } catch (Throwable e) {
150 | readSourceErrorsCount.incrementAndGet();
151 | onError(e);
152 | } finally {
153 | closeSource();
154 | notifyNewCacheDataAvailable(cachePercentage);
155 | }
156 | }
157 |
158 | private void closeSource() {
159 | try {
160 | source.close();
161 | } catch (ProxyCacheException e) {
162 | onError(new ProxyCacheException("Error closing source " + source, e));
163 | }
164 | }
165 |
166 | protected final void onError(final Throwable e) {
167 | Log.e(LOG_TAG, "ProxyCache error", e);
168 | handler.post(new ErrorDeliverer(e));
169 | }
170 |
171 | protected boolean isLogEnabled() {
172 | return logEnabled;
173 | }
174 |
175 | private class SourceReaderRunnable implements Runnable {
176 |
177 | @Override
178 | public void run() {
179 | readSource();
180 | }
181 | }
182 |
183 | private class ErrorDeliverer implements Runnable {
184 |
185 | private final Throwable error;
186 |
187 | public ErrorDeliverer(Throwable error) {
188 | this.error = error;
189 | }
190 |
191 | @Override
192 | public void run() {
193 | if (error instanceof ProxyCacheException) {
194 | if (cacheListener != null) {
195 | cacheListener.onError((ProxyCacheException) error);
196 | }
197 | } else {
198 | throw new RuntimeException("Unexpected error!", error);
199 | }
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/ProxyCacheException.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache;
2 |
3 | /**
4 | * Indicates any error in work of {@link ProxyCache}.
5 | *
6 | * @author Alexey Danilov
7 | */
8 | public class ProxyCacheException extends Exception {
9 |
10 | public ProxyCacheException(String message) {
11 | super(message);
12 | }
13 |
14 | public ProxyCacheException(String message, Throwable cause) {
15 | super(message, cause);
16 | }
17 |
18 | public ProxyCacheException(Throwable cause) {
19 | super(cause);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache;
2 |
3 | import android.text.TextUtils;
4 | import android.webkit.MimeTypeMap;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.util.Arrays;
9 |
10 | import static com.danikula.videocache.Preconditions.checkArgument;
11 | import static com.danikula.videocache.Preconditions.checkNotNull;
12 |
13 | /**
14 | * Just simple utils.
15 | *
16 | * @author Alexey Danilov (danikula@gmail.com).
17 | */
18 | class ProxyCacheUtils {
19 |
20 | static final String LOG_TAG = "ProxyCache";
21 | static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
22 | static final int MAX_ARRAY_PREVIEW = 16;
23 |
24 | static String getSupposablyMime(String url) {
25 | MimeTypeMap mimes = MimeTypeMap.getSingleton();
26 | String extension = MimeTypeMap.getFileExtensionFromUrl(url);
27 | return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension);
28 | }
29 |
30 | static void assertBuffer(byte[] buffer, long offset, int length) {
31 | checkNotNull(buffer, "Buffer must be not null!");
32 | checkArgument(offset >= 0, "Data offset must be positive!");
33 | checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]");
34 | }
35 |
36 | static String preview(byte[] data, int length) {
37 | int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0));
38 | byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength);
39 | String preview = Arrays.toString(dataRange);
40 | if (previewLength < length) {
41 | preview = preview.substring(0, preview.length() - 1) + ", ...]";
42 | }
43 | return preview;
44 | }
45 |
46 | static void createDirectory(File directory) throws IOException {
47 | checkNotNull(directory, "File must be not null!");
48 | if (directory.exists()) {
49 | checkArgument(directory.isDirectory(), "File is not directory!");
50 | } else {
51 | boolean isCreated = directory.mkdirs();
52 | if (!isCreated) {
53 | String error = String.format("Directory %s can't be created", directory.getAbsolutePath());
54 | throw new IOException(error);
55 | }
56 | }
57 | }
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/Source.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache;
2 |
3 | /**
4 | * Source for proxy.
5 | *
6 | * @author Alexey Danilov (danikula@gmail.com).
7 | */
8 | public interface Source {
9 |
10 | int available() throws ProxyCacheException;
11 |
12 | void open(int offset) throws ProxyCacheException;
13 |
14 | void close() throws ProxyCacheException;
15 |
16 | int read(byte[] buffer) throws ProxyCacheException;
17 | }
18 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 22
5 | buildToolsVersion '22.0.1'
6 |
7 | defaultConfig {
8 | applicationId "com.danikula.videocache.sample"
9 | minSdkVersion 15
10 | targetSdkVersion 22
11 | versionCode 1
12 | versionName '1.0'
13 | }
14 | }
15 |
16 | repositories {
17 | maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
18 |
19 | }
20 |
21 | dependencies {
22 | compile 'com.danikula:videocache:1.0.1'
23 | compile 'com.google.android.exoplayer:exoplayer:r1.3.3'
24 | }
25 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/com/exoplayer/player/DebugTrackRenderer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.com.exoplayer.player;
17 |
18 | import android.widget.TextView;
19 |
20 | import com.google.android.exoplayer.ExoPlaybackException;
21 | import com.google.android.exoplayer.MediaCodecTrackRenderer;
22 | import com.google.android.exoplayer.TrackRenderer;
23 | import com.google.android.exoplayer.chunk.Format;
24 |
25 | /**
26 | * A {@link TrackRenderer} that periodically updates debugging information displayed by a
27 | * {@link TextView}.
28 | */
29 | /* package */ class DebugTrackRenderer extends TrackRenderer implements Runnable {
30 |
31 | private final TextView textView;
32 | private final DemoPlayer player;
33 | private final MediaCodecTrackRenderer renderer;
34 |
35 | private volatile boolean pendingFailure;
36 | private volatile long currentPositionUs;
37 |
38 | public DebugTrackRenderer(TextView textView, DemoPlayer player,
39 | MediaCodecTrackRenderer renderer) {
40 | this.textView = textView;
41 | this.player = player;
42 | this.renderer = renderer;
43 | }
44 |
45 | public void injectFailure() {
46 | pendingFailure = true;
47 | }
48 |
49 | @Override
50 | protected boolean isEnded() {
51 | return true;
52 | }
53 |
54 | @Override
55 | protected boolean isReady() {
56 | return true;
57 | }
58 |
59 | @Override
60 | protected int doPrepare(long positionUs) throws ExoPlaybackException {
61 | maybeFail();
62 | return STATE_PREPARED;
63 | }
64 |
65 | @Override
66 | protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
67 | maybeFail();
68 | if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) {
69 | currentPositionUs = positionUs;
70 | textView.post(this);
71 | }
72 | }
73 |
74 | @Override
75 | public void run() {
76 | textView.setText(getRenderString());
77 | }
78 |
79 | private String getRenderString() {
80 | return getQualityString() + " " + renderer.codecCounters.getDebugString();
81 | }
82 |
83 | private String getQualityString() {
84 | Format format = player.getVideoFormat();
85 | return format == null ? "id:? br:? h:?"
86 | : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height;
87 | }
88 |
89 | @Override
90 | protected long getCurrentPositionUs() {
91 | return currentPositionUs;
92 | }
93 |
94 | @Override
95 | protected long getDurationUs() {
96 | return TrackRenderer.MATCH_LONGEST_US;
97 | }
98 |
99 | @Override
100 | protected long getBufferedPositionUs() {
101 | return TrackRenderer.END_OF_TRACK_US;
102 | }
103 |
104 | @Override
105 | protected void seekTo(long timeUs) {
106 | currentPositionUs = timeUs;
107 | }
108 |
109 | private void maybeFail() throws ExoPlaybackException {
110 | if (pendingFailure) {
111 | pendingFailure = false;
112 | throw new ExoPlaybackException("fail() was called on DebugTrackRenderer");
113 | }
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/com/exoplayer/player/DemoPlayer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.com.exoplayer.player;
17 |
18 | import android.media.MediaCodec.CryptoException;
19 | import android.os.Handler;
20 | import android.os.Looper;
21 | import android.view.Surface;
22 |
23 | import com.google.android.exoplayer.DummyTrackRenderer;
24 | import com.google.android.exoplayer.ExoPlaybackException;
25 | import com.google.android.exoplayer.ExoPlayer;
26 | import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
27 | import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
28 | import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
29 | import com.google.android.exoplayer.TrackRenderer;
30 | import com.google.android.exoplayer.audio.AudioTrack;
31 | import com.google.android.exoplayer.chunk.ChunkSampleSource;
32 | import com.google.android.exoplayer.chunk.Format;
33 | import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
34 | import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
35 | import com.google.android.exoplayer.hls.HlsSampleSource;
36 | import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
37 | import com.google.android.exoplayer.text.TextRenderer;
38 | import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
39 | import com.google.android.exoplayer.util.PlayerControl;
40 |
41 | import java.io.IOException;
42 | import java.util.Map;
43 | import java.util.concurrent.CopyOnWriteArrayList;
44 |
45 | /**
46 | * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
47 | * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
48 | * SmoothStreaming and so on).
49 | */
50 | public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
51 | HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
52 | MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
53 | StreamingDrmSessionManager.EventListener, TextRenderer {
54 |
55 | /**
56 | * Builds renderers for the player.
57 | */
58 | public interface RendererBuilder {
59 | /**
60 | * Constructs the necessary components for playback.
61 | *
62 | * @param player The parent player.
63 | * @param callback The callback to invoke with the constructed components.
64 | */
65 | void buildRenderers(DemoPlayer player, RendererBuilderCallback callback);
66 | }
67 |
68 | /**
69 | * A callback invoked by a {@link RendererBuilder}.
70 | */
71 | public interface RendererBuilderCallback {
72 | /**
73 | * Invoked with the results from a {@link RendererBuilder}.
74 | *
75 | * @param trackNames The names of the available tracks, indexed by {@link DemoPlayer} TYPE_*
76 | * constants. May be null if the track names are unknown. An individual element may be null
77 | * if the track names are unknown for the corresponding type.
78 | * @param multiTrackSources Sources capable of switching between multiple available tracks,
79 | * indexed by {@link DemoPlayer} TYPE_* constants. May be null if there are no types with
80 | * multiple tracks. An individual element may be null if it does not have multiple tracks.
81 | * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual
82 | * element may be null if there do not exist tracks of the corresponding type.
83 | */
84 | void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources,
85 | TrackRenderer[] renderers);
86 | /**
87 | * Invoked if a {@link RendererBuilder} encounters an error.
88 | *
89 | * @param e Describes the error.
90 | */
91 | void onRenderersError(Exception e);
92 | }
93 |
94 | /**
95 | * A listener for core events.
96 | */
97 | public interface Listener {
98 | void onStateChanged(boolean playWhenReady, int playbackState);
99 | void onError(Exception e);
100 | void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio);
101 | }
102 |
103 | /**
104 | * A listener for internal errors.
105 | *
106 | * These errors are not visible to the user, and hence this listener is provided for
107 | * informational purposes only. Note however that an internal error may cause a fatal
108 | * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
109 | * will be invoked.
110 | */
111 | public interface InternalErrorListener {
112 | void onRendererInitializationError(Exception e);
113 | void onAudioTrackInitializationError(AudioTrack.InitializationException e);
114 | void onAudioTrackWriteError(AudioTrack.WriteException e);
115 | void onDecoderInitializationError(DecoderInitializationException e);
116 | void onCryptoError(CryptoException e);
117 | void onLoadError(int sourceId, IOException e);
118 | void onDrmSessionManagerError(Exception e);
119 | }
120 |
121 | /**
122 | * A listener for debugging information.
123 | */
124 | public interface InfoListener {
125 | void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs);
126 | void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs);
127 | void onDroppedFrames(int count, long elapsed);
128 | void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
129 | void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
130 | int mediaStartTimeMs, int mediaEndTimeMs);
131 | void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
132 | int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
133 | void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
134 | long initializationDurationMs);
135 | }
136 |
137 | /**
138 | * A listener for receiving notifications of timed text.
139 | */
140 | public interface TextListener {
141 | void onText(String text);
142 | }
143 |
144 | /**
145 | * A listener for receiving ID3 metadata parsed from the media stream.
146 | */
147 | public interface Id3MetadataListener {
148 | void onId3Metadata(Map metadata);
149 | }
150 |
151 | // Constants pulled into this class for convenience.
152 | public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
153 | public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
154 | public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
155 | public static final int STATE_READY = ExoPlayer.STATE_READY;
156 | public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
157 |
158 | public static final int DISABLED_TRACK = -1;
159 | public static final int PRIMARY_TRACK = 0;
160 |
161 | public static final int RENDERER_COUNT = 5;
162 | public static final int TYPE_VIDEO = 0;
163 | public static final int TYPE_AUDIO = 1;
164 | public static final int TYPE_TEXT = 2;
165 | public static final int TYPE_TIMED_METADATA = 3;
166 | public static final int TYPE_DEBUG = 4;
167 |
168 | private static final int RENDERER_BUILDING_STATE_IDLE = 1;
169 | private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
170 | private static final int RENDERER_BUILDING_STATE_BUILT = 3;
171 |
172 | private final RendererBuilder rendererBuilder;
173 | private final ExoPlayer player;
174 | private final PlayerControl playerControl;
175 | private final Handler mainHandler;
176 | private final CopyOnWriteArrayList listeners;
177 |
178 | private int rendererBuildingState;
179 | private int lastReportedPlaybackState;
180 | private boolean lastReportedPlayWhenReady;
181 |
182 | private Surface surface;
183 | private InternalRendererBuilderCallback builderCallback;
184 | private TrackRenderer videoRenderer;
185 | private Format videoFormat;
186 | private int videoTrackToRestore;
187 |
188 | private MultiTrackChunkSource[] multiTrackSources;
189 | private String[][] trackNames;
190 | private int[] selectedTracks;
191 | private boolean backgrounded;
192 |
193 | private TextListener textListener;
194 | private Id3MetadataListener id3MetadataListener;
195 | private InternalErrorListener internalErrorListener;
196 | private InfoListener infoListener;
197 |
198 | public DemoPlayer(RendererBuilder rendererBuilder) {
199 | this.rendererBuilder = rendererBuilder;
200 | player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
201 | player.addListener(this);
202 | playerControl = new PlayerControl(player);
203 | mainHandler = new Handler();
204 | listeners = new CopyOnWriteArrayList();
205 | lastReportedPlaybackState = STATE_IDLE;
206 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
207 | selectedTracks = new int[RENDERER_COUNT];
208 | // Disable text initially.
209 | selectedTracks[TYPE_TEXT] = DISABLED_TRACK;
210 | }
211 |
212 | public PlayerControl getPlayerControl() {
213 | return playerControl;
214 | }
215 |
216 | public void addListener(Listener listener) {
217 | listeners.add(listener);
218 | }
219 |
220 | public void removeListener(Listener listener) {
221 | listeners.remove(listener);
222 | }
223 |
224 | public void setInternalErrorListener(InternalErrorListener listener) {
225 | internalErrorListener = listener;
226 | }
227 |
228 | public void setInfoListener(InfoListener listener) {
229 | infoListener = listener;
230 | }
231 |
232 | public void setTextListener(TextListener listener) {
233 | textListener = listener;
234 | }
235 |
236 | public void setMetadataListener(Id3MetadataListener listener) {
237 | id3MetadataListener = listener;
238 | }
239 |
240 | public void setSurface(Surface surface) {
241 | this.surface = surface;
242 | pushSurface(false);
243 | }
244 |
245 | public Surface getSurface() {
246 | return surface;
247 | }
248 |
249 | public void blockingClearSurface() {
250 | surface = null;
251 | pushSurface(true);
252 | }
253 |
254 | public String[] getTracks(int type) {
255 | return trackNames == null ? null : trackNames[type];
256 | }
257 |
258 | public int getSelectedTrackIndex(int type) {
259 | return selectedTracks[type];
260 | }
261 |
262 | public void selectTrack(int type, int index) {
263 | if (selectedTracks[type] == index) {
264 | return;
265 | }
266 | selectedTracks[type] = index;
267 | pushTrackSelection(type, true);
268 | if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) {
269 | textListener.onText(null);
270 | }
271 | }
272 |
273 | public Format getVideoFormat() {
274 | return videoFormat;
275 | }
276 |
277 | public void setBackgrounded(boolean backgrounded) {
278 | if (this.backgrounded == backgrounded) {
279 | return;
280 | }
281 | this.backgrounded = backgrounded;
282 | if (backgrounded) {
283 | videoTrackToRestore = getSelectedTrackIndex(TYPE_VIDEO);
284 | selectTrack(TYPE_VIDEO, DISABLED_TRACK);
285 | blockingClearSurface();
286 | } else {
287 | selectTrack(TYPE_VIDEO, videoTrackToRestore);
288 | }
289 | }
290 |
291 | public void prepare() {
292 | if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
293 | player.stop();
294 | }
295 | if (builderCallback != null) {
296 | builderCallback.cancel();
297 | }
298 | videoFormat = null;
299 | videoRenderer = null;
300 | multiTrackSources = null;
301 | rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
302 | maybeReportPlayerState();
303 | builderCallback = new InternalRendererBuilderCallback();
304 | rendererBuilder.buildRenderers(this, builderCallback);
305 | }
306 |
307 | /* package */ void onRenderers(String[][] trackNames,
308 | MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) {
309 | builderCallback = null;
310 | // Normalize the results.
311 | if (trackNames == null) {
312 | trackNames = new String[RENDERER_COUNT][];
313 | }
314 | if (multiTrackSources == null) {
315 | multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT];
316 | }
317 | for (int i = 0; i < RENDERER_COUNT; i++) {
318 | if (renderers[i] == null) {
319 | // Convert a null renderer to a dummy renderer.
320 | renderers[i] = new DummyTrackRenderer();
321 | } else if (trackNames[i] == null) {
322 | // We have a renderer so we must have at least one track, but the names are unknown.
323 | // Initialize the correct number of null track names.
324 | int trackCount = multiTrackSources[i] == null ? 1 : multiTrackSources[i].getTrackCount();
325 | trackNames[i] = new String[trackCount];
326 | }
327 | }
328 | // Complete preparation.
329 | this.trackNames = trackNames;
330 | this.videoRenderer = renderers[TYPE_VIDEO];
331 | this.multiTrackSources = multiTrackSources;
332 | pushSurface(false);
333 | pushTrackSelection(TYPE_VIDEO, true);
334 | pushTrackSelection(TYPE_AUDIO, true);
335 | pushTrackSelection(TYPE_TEXT, true);
336 | player.prepare(renderers);
337 | rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
338 | }
339 |
340 | /* package */ void onRenderersError(Exception e) {
341 | builderCallback = null;
342 | if (internalErrorListener != null) {
343 | internalErrorListener.onRendererInitializationError(e);
344 | }
345 | for (Listener listener : listeners) {
346 | listener.onError(e);
347 | }
348 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
349 | maybeReportPlayerState();
350 | }
351 |
352 | public void setPlayWhenReady(boolean playWhenReady) {
353 | player.setPlayWhenReady(playWhenReady);
354 | }
355 |
356 | public void seekTo(long positionMs) {
357 | player.seekTo(positionMs);
358 | }
359 |
360 | public void release() {
361 | if (builderCallback != null) {
362 | builderCallback.cancel();
363 | builderCallback = null;
364 | }
365 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
366 | surface = null;
367 | player.release();
368 | }
369 |
370 |
371 | public int getPlaybackState() {
372 | if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
373 | return ExoPlayer.STATE_PREPARING;
374 | }
375 | int playerState = player.getPlaybackState();
376 | if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT
377 | && rendererBuildingState == RENDERER_BUILDING_STATE_IDLE) {
378 | // This is an edge case where the renderers are built, but are still being passed to the
379 | // player's playback thread.
380 | return ExoPlayer.STATE_PREPARING;
381 | }
382 | return playerState;
383 | }
384 |
385 | public long getCurrentPosition() {
386 | return player.getCurrentPosition();
387 | }
388 |
389 | public long getDuration() {
390 | return player.getDuration();
391 | }
392 |
393 | public int getBufferedPercentage() {
394 | return player.getBufferedPercentage();
395 | }
396 |
397 | public boolean getPlayWhenReady() {
398 | return player.getPlayWhenReady();
399 | }
400 |
401 | /* package */ Looper getPlaybackLooper() {
402 | return player.getPlaybackLooper();
403 | }
404 |
405 | /* package */ Handler getMainHandler() {
406 | return mainHandler;
407 | }
408 |
409 | @Override
410 | public void onPlayerStateChanged(boolean playWhenReady, int state) {
411 | maybeReportPlayerState();
412 | }
413 |
414 | @Override
415 | public void onPlayerError(ExoPlaybackException exception) {
416 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
417 | for (Listener listener : listeners) {
418 | listener.onError(exception);
419 | }
420 | }
421 |
422 | @Override
423 | public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
424 | for (Listener listener : listeners) {
425 | listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio);
426 | }
427 | }
428 |
429 | @Override
430 | public void onDroppedFrames(int count, long elapsed) {
431 | if (infoListener != null) {
432 | infoListener.onDroppedFrames(count, elapsed);
433 | }
434 | }
435 |
436 | @Override
437 | public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
438 | if (infoListener != null) {
439 | infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
440 | }
441 | }
442 |
443 | @Override
444 | public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs) {
445 | if (infoListener == null) {
446 | return;
447 | }
448 | if (sourceId == TYPE_VIDEO) {
449 | videoFormat = format;
450 | infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
451 | } else if (sourceId == TYPE_AUDIO) {
452 | infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
453 | }
454 | }
455 |
456 | @Override
457 | public void onDrmSessionManagerError(Exception e) {
458 | if (internalErrorListener != null) {
459 | internalErrorListener.onDrmSessionManagerError(e);
460 | }
461 | }
462 |
463 | @Override
464 | public void onDecoderInitializationError(DecoderInitializationException e) {
465 | if (internalErrorListener != null) {
466 | internalErrorListener.onDecoderInitializationError(e);
467 | }
468 | }
469 |
470 | @Override
471 | public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
472 | if (internalErrorListener != null) {
473 | internalErrorListener.onAudioTrackInitializationError(e);
474 | }
475 | }
476 |
477 | @Override
478 | public void onAudioTrackWriteError(AudioTrack.WriteException e) {
479 | if (internalErrorListener != null) {
480 | internalErrorListener.onAudioTrackWriteError(e);
481 | }
482 | }
483 |
484 | @Override
485 | public void onCryptoError(CryptoException e) {
486 | if (internalErrorListener != null) {
487 | internalErrorListener.onCryptoError(e);
488 | }
489 | }
490 |
491 | @Override
492 | public void onDecoderInitialized(
493 | String decoderName,
494 | long elapsedRealtimeMs,
495 | long initializationDurationMs) {
496 | if (infoListener != null) {
497 | infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
498 | }
499 | }
500 |
501 | @Override
502 | public void onLoadError(int sourceId, IOException e) {
503 | if (internalErrorListener != null) {
504 | internalErrorListener.onLoadError(sourceId, e);
505 | }
506 | }
507 |
508 | @Override
509 | public void onText(String text) {
510 | processText(text);
511 | }
512 |
513 | /* package */ MetadataTrackRenderer.MetadataRenderer