1/* 2 * Copyright (C) 2016 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 17package com.android.usbtuner.exoplayer; 18 19import android.media.MediaDataSource; 20import android.media.MediaExtractor; 21import android.os.ConditionVariable; 22import android.os.SystemClock; 23import android.util.Log; 24 25import com.google.android.exoplayer.SampleHolder; 26import com.android.usbtuner.exoplayer.cache.CacheManager; 27import com.android.usbtuner.exoplayer.cache.RecordingSampleBuffer; 28import com.android.usbtuner.tvinput.PlaybackCacheListener; 29 30import java.io.IOException; 31import java.util.ArrayList; 32import java.util.List; 33import java.util.Locale; 34import java.util.concurrent.atomic.AtomicLong; 35 36/** 37 * Records live streams on the disk for DVR. 38 */ 39public class Recorder { 40 private static final String TAG = "Recorder"; 41 42 // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice. 43 private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2; 44 private static final AtomicLong ID_COUNTER = new AtomicLong(0); 45 46 private final MediaDataSource mDataSource; 47 private final MediaExtractor mMediaExtractor; 48 private final ExtractorThread mExtractorThread; 49 private int mTrackCount; 50 private List<android.media.MediaFormat> mMediaFormats; 51 52 private final CacheManager.SampleBuffer mSampleBuffer; 53 54 private boolean mReleased = false; 55 private boolean mResultNotified = false; 56 private final long mId; 57 58 private final RecordListener mRecordListener; 59 60 /** 61 * Listeners for events which happens during the recording. 62 */ 63 public interface RecordListener { 64 65 /** 66 * Notifies recording completion. 67 * 68 * @param success {@code true} when the recording succeeded, {@code false} otherwise 69 */ 70 void notifyRecordingFinished(boolean success); 71 } 72 73 /** 74 * Create a recorder for a {@link android.media.MediaDataSource}. 75 * 76 * @param source {@link android.media.MediaDataSource} to record from 77 * @param cacheManager the manager for recording samples to physical storage 78 * @param cacheListener the {@link com.android.usbtuner.tvinput.PlaybackCacheListener} 79 * to notify cache storage status change 80 * @param recordListener RecordListener to notify events during the recording 81 */ 82 public Recorder(MediaDataSource source, CacheManager cacheManager, 83 PlaybackCacheListener cacheListener, RecordListener recordListener) { 84 mDataSource = source; 85 mMediaExtractor = new MediaExtractor(); 86 mExtractorThread = new ExtractorThread(); 87 mRecordListener = recordListener; 88 89 mSampleBuffer = new RecordingSampleBuffer(cacheManager, cacheListener, false, 90 RecordingSampleBuffer.CACHE_REASON_RECORDING); 91 mId = ID_COUNTER.incrementAndGet(); 92 } 93 94 private class ExtractorThread extends Thread { 95 private volatile boolean mQuitRequested = false; 96 97 public ExtractorThread() { 98 super("ExtractorThread"); 99 } 100 101 @Override 102 public void run() { 103 SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); 104 sample.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE); 105 ConditionVariable conditionVariable = new ConditionVariable(); 106 while (!mQuitRequested) { 107 fetchSample(sample, conditionVariable); 108 } 109 cleanUp(); 110 } 111 112 private void fetchSample(SampleHolder sample, ConditionVariable conditionVariable) { 113 int index = mMediaExtractor.getSampleTrackIndex(); 114 if (index < 0) { 115 Log.i(TAG, "EoS"); 116 mQuitRequested = true; 117 mSampleBuffer.setEos(); 118 return; 119 } 120 sample.data.clear(); 121 sample.size = mMediaExtractor.readSampleData(sample.data, 0); 122 if (sample.size < 0 || sample.size > SAMPLE_BUFFER_SIZE) { 123 // Should not happen 124 Log.e(TAG, "Invalid sample size: " + sample.size); 125 mMediaExtractor.advance(); 126 return; 127 } 128 sample.data.position(sample.size); 129 sample.timeUs = mMediaExtractor.getSampleTime(); 130 sample.flags = mMediaExtractor.getSampleFlags(); 131 132 mMediaExtractor.advance(); 133 try { 134 queueSample(index, sample, conditionVariable); 135 } catch (IOException e) { 136 mQuitRequested = true; 137 mSampleBuffer.setEos(); 138 } 139 } 140 141 public void quit() { 142 mQuitRequested = true; 143 } 144 } 145 146 private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) 147 throws IOException { 148 long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); 149 mSampleBuffer.writeSample(index, sample, conditionVariable); 150 151 // Check if the storage has enough bandwidth for recording. Otherwise we disable it 152 // and notify the slowness. 153 if (mSampleBuffer.isWriteSpeedSlow(sample.size, 154 SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { 155 Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); 156 throw new IOException("Disk is too slow"); 157 } 158 } 159 160 /** 161 * Prepares a recording. 162 * 163 * @return {@code true} when preparation finished successfully, {@code false} otherwise 164 * @throws IOException 165 */ 166 public boolean prepare() throws IOException { 167 synchronized (this) { 168 mMediaExtractor.setDataSource(mDataSource); 169 170 mTrackCount = mMediaExtractor.getTrackCount(); 171 List<String> ids = new ArrayList<>(); 172 mMediaFormats = new ArrayList<>(); 173 for (int i = 0; i < mTrackCount; i++) { 174 ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); 175 android.media.MediaFormat format = mMediaExtractor.getTrackFormat(i); 176 mMediaExtractor.selectTrack(i); 177 mMediaFormats.add(format); 178 } 179 mSampleBuffer.init(ids, mMediaFormats); 180 } 181 mExtractorThread.start(); 182 return true; 183 } 184 185 /** 186 * Releases all the resources which were used in the recording. 187 */ 188 public void release() { 189 synchronized (this) { 190 mReleased = true; 191 } 192 if (mExtractorThread.isAlive()) { 193 mExtractorThread.quit(); 194 // We don't join here to prevent hang --- MediaExtractor is released at the thread. 195 } else { 196 cleanUp(); 197 } 198 } 199 200 private synchronized void cleanUp() { 201 if (!mReleased) { 202 if (!mResultNotified) { 203 mRecordListener.notifyRecordingFinished(false); 204 mResultNotified = true; 205 } 206 return; 207 } 208 mSampleBuffer.release(); 209 if (!mResultNotified) { 210 mRecordListener.notifyRecordingFinished(true); 211 mResultNotified = true; 212 } 213 mMediaExtractor.release(); 214 } 215 216} 217