1 /* 2 * Copyright (C) 2015 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 17 package com.android.tv.tuner.exoplayer; 18 19 import android.net.Uri; 20 import android.os.ConditionVariable; 21 import android.os.Handler; 22 import android.os.HandlerThread; 23 import android.os.Looper; 24 import android.os.Message; 25 import android.os.SystemClock; 26 27 import com.google.android.exoplayer.C; 28 import com.google.android.exoplayer.MediaFormat; 29 import com.google.android.exoplayer.MediaFormatHolder; 30 import com.google.android.exoplayer.SampleHolder; 31 import com.google.android.exoplayer.SampleSource; 32 import com.google.android.exoplayer.extractor.ExtractorSampleSource; 33 import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener; 34 import com.google.android.exoplayer.upstream.Allocator; 35 import com.google.android.exoplayer.upstream.DataSource; 36 import com.google.android.exoplayer.upstream.DefaultAllocator; 37 import com.android.tv.tuner.exoplayer.buffer.BufferManager; 38 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; 39 import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; 40 import com.android.tv.tuner.tvinput.PlaybackBufferListener; 41 42 import java.io.IOException; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.concurrent.atomic.AtomicBoolean; 48 import java.util.concurrent.atomic.AtomicLong; 49 50 /** 51 * A class that extracts samples from a live broadcast stream while storing the sample on the disk. 52 * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}. 53 */ 54 public class ExoPlayerSampleExtractor implements SampleExtractor { 55 private static final String TAG = "ExoPlayerSampleExtracto"; 56 57 // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer. 58 private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024; 59 // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer. 60 private static final int BUFFER_SEGMENT_COUNT = 256; 61 62 private final HandlerThread mSourceReaderThread; 63 private final long mId; 64 65 private final Handler.Callback mSourceReaderWorker; 66 67 private BufferManager.SampleBuffer mSampleBuffer; 68 private Handler mSourceReaderHandler; 69 private volatile boolean mPrepared; 70 private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); 71 private IOException mExceptionOnPrepare; 72 private List<MediaFormat> mTrackFormats; 73 private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>(); 74 private OnCompletionListener mOnCompletionListener; 75 private Handler mOnCompletionListenerHandler; 76 private IOException mError; 77 ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean isRecording)78 public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager, 79 PlaybackBufferListener bufferListener, boolean isRecording) { 80 // It'll be used as a timeshift file chunk name's prefix. 81 mId = System.currentTimeMillis(); 82 Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES); 83 84 EventListener eventListener = new EventListener() { 85 86 @Override 87 public void onLoadError(int sourceId, IOException e) { 88 mError = e; 89 } 90 }; 91 92 mSourceReaderThread = new HandlerThread("SourceReaderThread"); 93 mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source, 94 allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES, 95 // Do not create a handler if we not on a looper. e.g. test. 96 Looper.myLooper() != null ? new Handler() : null, 97 eventListener, 0)); 98 if (isRecording) { 99 mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, 100 RecordingSampleBuffer.BUFFER_REASON_RECORDING); 101 } else { 102 if (bufferManager == null || bufferManager.isDisabled()) { 103 mSampleBuffer = new SimpleSampleBuffer(bufferListener); 104 } else { 105 mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, 106 RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); 107 } 108 } 109 } 110 111 @Override setOnCompletionListener(OnCompletionListener listener, Handler handler)112 public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { 113 mOnCompletionListener = listener; 114 mOnCompletionListenerHandler = handler; 115 } 116 117 private class SourceReaderWorker implements Handler.Callback { 118 public static final int MSG_PREPARE = 1; 119 public static final int MSG_FETCH_SAMPLES = 2; 120 public static final int MSG_RELEASE = 3; 121 private static final int RETRY_INTERVAL_MS = 50; 122 123 private final SampleSource mSampleSource; 124 private SampleSource.SampleSourceReader mSampleSourceReader; 125 private boolean[] mTrackMetEos; 126 private boolean mMetEos = false; 127 private long mCurrentPosition; 128 SourceReaderWorker(SampleSource sampleSource)129 public SourceReaderWorker(SampleSource sampleSource) { 130 mSampleSource = sampleSource; 131 } 132 133 @Override handleMessage(Message message)134 public boolean handleMessage(Message message) { 135 switch (message.what) { 136 case MSG_PREPARE: 137 mPrepared = prepare(); 138 if (!mPrepared && mExceptionOnPrepare == null) { 139 mSourceReaderHandler 140 .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS); 141 } else{ 142 mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); 143 } 144 return true; 145 case MSG_FETCH_SAMPLES: 146 boolean didSomething = false; 147 SampleHolder sample = new SampleHolder( 148 SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); 149 ConditionVariable conditionVariable = new ConditionVariable(); 150 int trackCount = mSampleSourceReader.getTrackCount(); 151 for (int i = 0; i < trackCount; ++i) { 152 if (!mTrackMetEos[i] && SampleSource.NOTHING_READ 153 != fetchSample(i, sample, conditionVariable)) { 154 if (mMetEos) { 155 // If mMetEos was on during fetchSample() due to an error, 156 // fetching from other tracks is not necessary. 157 break; 158 } 159 didSomething = true; 160 } 161 } 162 if (!mMetEos) { 163 if (didSomething) { 164 mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); 165 } else { 166 mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES, 167 RETRY_INTERVAL_MS); 168 } 169 } else { 170 notifyCompletionIfNeeded(false); 171 } 172 return true; 173 case MSG_RELEASE: 174 if (mSampleSourceReader != null) { 175 if (mPrepared) { 176 // ExtractorSampleSource expects all the tracks should be disabled 177 // before releasing. 178 int count = mSampleSourceReader.getTrackCount(); 179 for (int i = 0; i < count; ++i) { 180 mSampleSourceReader.disable(i); 181 } 182 } 183 mSampleSourceReader.release(); 184 mSampleSourceReader = null; 185 } 186 cleanUp(); 187 mSourceReaderHandler.removeCallbacksAndMessages(null); 188 return true; 189 } 190 return false; 191 } 192 prepare()193 private boolean prepare() { 194 if (mSampleSourceReader == null) { 195 mSampleSourceReader = mSampleSource.register(); 196 } 197 if(!mSampleSourceReader.prepare(0)) { 198 return false; 199 } 200 if (mTrackFormats == null) { 201 int trackCount = mSampleSourceReader.getTrackCount(); 202 mTrackMetEos = new boolean[trackCount]; 203 List<MediaFormat> trackFormats = new ArrayList<>(); 204 for (int i = 0; i < trackCount; i++) { 205 trackFormats.add(mSampleSourceReader.getFormat(i)); 206 mSampleSourceReader.enable(i, 0); 207 208 } 209 mTrackFormats = trackFormats; 210 List<String> ids = new ArrayList<>(); 211 for (int i = 0; i < mTrackFormats.size(); i++) { 212 ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); 213 } 214 try { 215 mSampleBuffer.init(ids, mTrackFormats); 216 } catch (IOException e) { 217 // In this case, we will not schedule any further operation. 218 // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will 219 // call release() eventually. 220 mExceptionOnPrepare = e; 221 return false; 222 } 223 } 224 return true; 225 } 226 fetchSample(int track, SampleHolder sample, ConditionVariable conditionVariable)227 private int fetchSample(int track, SampleHolder sample, 228 ConditionVariable conditionVariable) { 229 mSampleSourceReader.continueBuffering(track, mCurrentPosition); 230 231 MediaFormatHolder formatHolder = new MediaFormatHolder(); 232 sample.clearData(); 233 int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample); 234 if (ret == SampleSource.SAMPLE_READ) { 235 if (mCurrentPosition < sample.timeUs) { 236 mCurrentPosition = sample.timeUs; 237 } 238 try { 239 Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track); 240 if (lastExtractedPositionUs == null) { 241 mLastExtractedPositionUsMap.put(track, sample.timeUs); 242 } else { 243 mLastExtractedPositionUsMap.put(track, 244 Math.max(lastExtractedPositionUs, sample.timeUs)); 245 } 246 queueSample(track, sample, conditionVariable); 247 } catch (IOException e) { 248 mLastExtractedPositionUsMap.clear(); 249 mMetEos = true; 250 mSampleBuffer.setEos(); 251 } 252 } else if (ret == SampleSource.END_OF_STREAM) { 253 mTrackMetEos[track] = true; 254 for (int i = 0; i < mTrackMetEos.length; ++i) { 255 if (!mTrackMetEos[i]) { 256 break; 257 } 258 if (i == mTrackMetEos.length -1) { 259 mMetEos = true; 260 mSampleBuffer.setEos(); 261 } 262 } 263 } 264 // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263 265 return ret; 266 } 267 } 268 queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)269 private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) 270 throws IOException { 271 long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); 272 mSampleBuffer.writeSample(index, sample, conditionVariable); 273 274 // Checks whether the storage has enough bandwidth for recording samples. 275 if (mSampleBuffer.isWriteSpeedSlow(sample.size, 276 SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { 277 mSampleBuffer.handleWriteSpeedSlow(); 278 } 279 } 280 281 @Override maybeThrowError()282 public void maybeThrowError() throws IOException { 283 if (mError != null) { 284 IOException e = mError; 285 mError = null; 286 throw e; 287 } 288 } 289 290 @Override prepare()291 public boolean prepare() throws IOException { 292 if (!mSourceReaderThread.isAlive()) { 293 mSourceReaderThread.start(); 294 mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(), 295 mSourceReaderWorker); 296 mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE); 297 } 298 if (mExceptionOnPrepare != null) { 299 throw mExceptionOnPrepare; 300 } 301 return mPrepared; 302 } 303 304 @Override getTrackFormats()305 public List<MediaFormat> getTrackFormats() { 306 return mTrackFormats; 307 } 308 309 @Override getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder)310 public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { 311 outMediaFormatHolder.format = mTrackFormats.get(track); 312 outMediaFormatHolder.drmInitData = null; 313 } 314 315 @Override selectTrack(int index)316 public void selectTrack(int index) { 317 mSampleBuffer.selectTrack(index); 318 } 319 320 @Override deselectTrack(int index)321 public void deselectTrack(int index) { 322 mSampleBuffer.deselectTrack(index); 323 } 324 325 @Override getBufferedPositionUs()326 public long getBufferedPositionUs() { 327 return mSampleBuffer.getBufferedPositionUs(); 328 } 329 330 @Override continueBuffering(long positionUs)331 public boolean continueBuffering(long positionUs) { 332 return mSampleBuffer.continueBuffering(positionUs); 333 } 334 335 @Override seekTo(long positionUs)336 public void seekTo(long positionUs) { 337 mSampleBuffer.seekTo(positionUs); 338 } 339 340 @Override readSample(int track, SampleHolder sampleHolder)341 public int readSample(int track, SampleHolder sampleHolder) { 342 return mSampleBuffer.readSample(track, sampleHolder); 343 } 344 345 @Override release()346 public void release() { 347 if (mSourceReaderThread.isAlive()) { 348 mSourceReaderHandler.removeCallbacksAndMessages(null); 349 mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE); 350 mSourceReaderThread.quitSafely(); 351 // Return early in this case so that session worker can start working on the next 352 // request as early as it can. The clean up will be done in the reader thread while 353 // handling MSG_RELEASE. 354 } else { 355 cleanUp(); 356 } 357 } 358 cleanUp()359 private void cleanUp() { 360 boolean result = true; 361 try { 362 if (mSampleBuffer != null) { 363 mSampleBuffer.release(); 364 mSampleBuffer = null; 365 } 366 } catch (IOException e) { 367 result = false; 368 } 369 notifyCompletionIfNeeded(result); 370 setOnCompletionListener(null, null); 371 } 372 notifyCompletionIfNeeded(final boolean result)373 private void notifyCompletionIfNeeded(final boolean result) { 374 if (!mOnCompletionCalled.getAndSet(true)) { 375 final OnCompletionListener listener = mOnCompletionListener; 376 final long lastExtractedPositionUs = getLastExtractedPositionUs(); 377 if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { 378 mOnCompletionListenerHandler.post(new Runnable() { 379 @Override 380 public void run() { 381 listener.onCompletion(result, lastExtractedPositionUs); 382 } 383 }); 384 } 385 } 386 } 387 getLastExtractedPositionUs()388 private long getLastExtractedPositionUs() { 389 long lastExtractedPositionUs = Long.MAX_VALUE; 390 for (long value : mLastExtractedPositionUsMap.values()) { 391 lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value); 392 } 393 if (lastExtractedPositionUs == Long.MAX_VALUE) { 394 lastExtractedPositionUs = C.UNKNOWN_TIME_US; 395 } 396 return lastExtractedPositionUs; 397 } 398 } 399