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.usbtuner.exoplayer; 18 19 import android.media.MediaDataSource; 20 21 import com.google.android.exoplayer.MediaFormat; 22 import com.google.android.exoplayer.MediaFormatHolder; 23 import com.google.android.exoplayer.MediaFormatUtil; 24 import com.google.android.exoplayer.SampleHolder; 25 import com.google.android.exoplayer.SampleSource; 26 import com.google.android.exoplayer.util.MimeTypes; 27 import com.android.usbtuner.exoplayer.cache.CacheManager; 28 import com.android.usbtuner.tvinput.PlaybackCacheListener; 29 30 import java.io.IOException; 31 import java.nio.ByteBuffer; 32 33 /** 34 * Extracts samples from {@link MediaDataSource} for MPEG-TS streams. 35 */ 36 public final class MpegTsSampleSourceExtractor implements SampleExtractor { 37 public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708"; 38 39 private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8; 40 41 private final SampleExtractor mSampleExtractor; 42 private MediaFormat[] mTrackFormats; 43 private boolean[] mGotEos; 44 private int mVideoTrackIndex; 45 private int mCea708TextTrackIndex; 46 private boolean mCea708TextTrackSelected; 47 private ByteBuffer mCea708CcBuffer; 48 49 private long mCea708PresentationTimeUs; 50 private CcParser mCcParser; 51 52 private void init() { 53 mVideoTrackIndex = -1; 54 mCea708TextTrackIndex = -1; 55 mCea708CcBuffer = ByteBuffer.allocate(CC_BUFFER_SIZE_IN_BYTES); 56 mCea708PresentationTimeUs = -1; 57 mCea708TextTrackSelected = false; 58 } 59 60 /** 61 * Creates MpegTsSampleSourceExtractor for {@link MediaDataSource}. 62 * 63 * @param source the {@link MediaDataSource} to extract from 64 * @param cacheManager the manager for reading & writing samples backed by physical storage 65 * @param cacheListener the {@link com.android.usbtuner.tvinput.PlaybackCacheListener} 66 * to notify cache storage status change 67 */ 68 public MpegTsSampleSourceExtractor(MediaDataSource source, 69 CacheManager cacheManager, PlaybackCacheListener cacheListener) { 70 if (cacheManager == null || cacheManager.isDisabled()) { 71 mSampleExtractor = 72 new PlaySampleExtractor(source, cacheManager, cacheListener, false); 73 74 } else { 75 mSampleExtractor = 76 new PlaySampleExtractor(source, cacheManager, cacheListener, true); 77 } 78 init(); 79 } 80 81 /** 82 * Creates MpegTsSampleSourceExtractor for a recorded program. 83 * 84 * @param cacheManager the samples provider which is stored in physical storage 85 * @param cacheListener the {@link com.android.usbtuner.tvinput.PlaybackCacheListener} 86 * to notify cache storage status change 87 */ 88 public MpegTsSampleSourceExtractor(CacheManager cacheManager, 89 PlaybackCacheListener cacheListener) { 90 mSampleExtractor = new ReplaySampleSourceExtractor(cacheManager, cacheListener); 91 init(); 92 } 93 94 @Override 95 public boolean prepare() throws IOException { 96 if(!mSampleExtractor.prepare()) { 97 return false; 98 } 99 MediaFormat trackFormats[] = mSampleExtractor.getTrackFormats(); 100 int trackCount = trackFormats.length; 101 mGotEos = new boolean[trackCount]; 102 103 for (int i = 0; i < trackCount; ++i) { 104 String mime = trackFormats[i].mimeType; 105 if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) { 106 mVideoTrackIndex = i; 107 if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) { 108 mCcParser = new Mpeg2CcParser(); 109 } else if (android.media.MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { 110 mCcParser = new H264CcParser(); 111 } 112 } 113 } 114 115 if (mVideoTrackIndex != -1) { 116 mCea708TextTrackIndex = trackCount; 117 } 118 mTrackFormats = new MediaFormat[mCea708TextTrackIndex < 0 ? trackCount : trackCount + 1]; 119 System.arraycopy(trackFormats, 0, mTrackFormats, 0, trackCount); 120 if (mCea708TextTrackIndex >= 0) { 121 mTrackFormats[trackCount] = MediaFormatUtil.createTextMediaFormat(MIMETYPE_TEXT_CEA_708, 122 mTrackFormats[0].durationUs); 123 } 124 return true; 125 } 126 127 @Override 128 public MediaFormat[] getTrackFormats() { 129 return mTrackFormats; 130 } 131 132 @Override 133 public void selectTrack(int index) { 134 if (index == mCea708TextTrackIndex) { 135 mCea708TextTrackSelected = true; 136 return; 137 } 138 mSampleExtractor.selectTrack(index); 139 } 140 141 @Override 142 public void deselectTrack(int index) { 143 if (index == mCea708TextTrackIndex) { 144 mCea708TextTrackSelected = false; 145 return; 146 } 147 mSampleExtractor.deselectTrack(index); 148 } 149 150 @Override 151 public long getBufferedPositionUs() { 152 return mSampleExtractor.getBufferedPositionUs(); 153 } 154 155 @Override 156 public void seekTo(long positionUs) { 157 mSampleExtractor.seekTo(positionUs); 158 } 159 160 @Override 161 public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { 162 if (track != mCea708TextTrackIndex) { 163 mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder); 164 } 165 } 166 167 @Override 168 public int readSample(int track, SampleHolder sampleHolder) { 169 if (track == mCea708TextTrackIndex) { 170 if (mCea708TextTrackSelected && mCea708CcBuffer.position() > 0) { 171 mCea708CcBuffer.flip(); 172 sampleHolder.timeUs = mCea708PresentationTimeUs; 173 sampleHolder.data.put(mCea708CcBuffer); 174 mCea708CcBuffer.clear(); 175 return SampleSource.SAMPLE_READ; 176 } else { 177 return mVideoTrackIndex < 0 || mGotEos[mVideoTrackIndex] 178 ? SampleSource.END_OF_STREAM : SampleSource.NOTHING_READ; 179 } 180 } 181 182 // Should read CC track first. 183 if (mCea708TextTrackSelected && mCea708CcBuffer.position() > 0) { 184 return mGotEos[track] ? SampleSource.END_OF_STREAM : SampleSource.NOTHING_READ; 185 } 186 187 int result = mSampleExtractor.readSample(track, sampleHolder); 188 switch (result) { 189 case SampleSource.END_OF_STREAM: { 190 mGotEos[track] = true; 191 break; 192 } 193 case SampleSource.SAMPLE_READ: { 194 if (mCea708TextTrackSelected && track == mVideoTrackIndex 195 && sampleHolder.data != null) { 196 mCcParser.mayParseClosedCaption(sampleHolder.data, sampleHolder.timeUs); 197 } 198 break; 199 } 200 } 201 return result; 202 } 203 204 @Override 205 public void release() { 206 mSampleExtractor.release(); 207 mVideoTrackIndex = -1; 208 mCea708TextTrackIndex = -1; 209 mCea708TextTrackSelected = false; 210 } 211 212 @Override 213 public boolean continueBuffering(long positionUs) { 214 return mSampleExtractor.continueBuffering(positionUs); 215 } 216 217 private abstract class CcParser { 218 abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs); 219 220 protected void parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) { 221 // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9. 222 int pos = offset; 223 if (pos + 2 >= buffer.position()) { 224 return; 225 } 226 boolean processCcDataFlag = (buffer.get(pos) & 64) != 0; 227 int ccCount = buffer.get(pos) & 0x1f; 228 pos += 2; 229 if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) { 230 return; 231 } 232 for (int i = 0; i < 3 * ccCount; i++) { 233 mCea708CcBuffer.put(buffer.get(pos + i)); 234 } 235 mCea708PresentationTimeUs = presentationTimeUs; 236 } 237 } 238 239 private class Mpeg2CcParser extends CcParser { 240 @Override 241 public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { 242 int pos = 0; 243 while (pos + 9 < buffer.position()) { 244 // Find the start prefix code of private user data. 245 if (buffer.get(pos) == 0 246 && buffer.get(pos + 1) == 0 247 && buffer.get(pos + 2) == 1 248 && (buffer.get(pos + 3) & 0xff) == 0xb2) { 249 // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user 250 // identifier and user data type code 3. 251 if (buffer.get(pos + 4) == 'G' 252 && buffer.get(pos + 5) == 'A' 253 && buffer.get(pos + 6) == '9' 254 && buffer.get(pos + 7) == '4' 255 && buffer.get(pos + 8) == 3) { 256 parseClosedCaption(buffer, pos + 9, presentationTimeUs); 257 } 258 pos += 9; 259 } else { 260 ++pos; 261 } 262 } 263 } 264 } 265 266 private class H264CcParser extends CcParser { 267 @Override 268 public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { 269 int pos = 0; 270 while (pos + 7 < buffer.position()) { 271 // Find the start prefix code of a NAL Unit. 272 if (buffer.get(pos) == 0 273 && buffer.get(pos + 1) == 0 274 && buffer.get(pos + 2) == 1) { 275 int nalType = buffer.get(pos + 3) & 0x1f; 276 int payloadType = buffer.get(pos + 4) & 0xff; 277 278 // ATSC closed caption data embedded in H264 private user data has NAL type 6, 279 // payload type 4, and 'GA94' user identifier for ATSC. 280 if (nalType == 6 && payloadType == 4 && buffer.get(pos + 9) == 'G' 281 && buffer.get(pos + 10) == 'A' 282 && buffer.get(pos + 11) == '9' 283 && buffer.get(pos + 12) == '4') { 284 parseClosedCaption(buffer, pos + 14, presentationTimeUs); 285 } 286 pos += 7; 287 } else { 288 ++pos; 289 } 290 } 291 } 292 } 293 } 294