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