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 package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; 17 18 import org.mozilla.thirdparty.com.google.android.exoplayer2.C; 19 import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; 20 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; 21 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; 22 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; 23 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; 24 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; 25 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; 26 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; 27 import java.io.IOException; 28 29 /** StreamReader abstract class. */ 30 @SuppressWarnings("UngroupedOverloads") 31 /* package */ abstract class StreamReader { 32 33 private static final int STATE_READ_HEADERS = 0; 34 private static final int STATE_SKIP_HEADERS = 1; 35 private static final int STATE_READ_PAYLOAD = 2; 36 private static final int STATE_END_OF_INPUT = 3; 37 38 static class SetupData { 39 Format format; 40 OggSeeker oggSeeker; 41 } 42 43 private final OggPacket oggPacket; 44 45 private TrackOutput trackOutput; 46 private ExtractorOutput extractorOutput; 47 private OggSeeker oggSeeker; 48 private long targetGranule; 49 private long payloadStartPosition; 50 private long currentGranule; 51 private int state; 52 private int sampleRate; 53 private SetupData setupData; 54 private long lengthOfReadPacket; 55 private boolean seekMapSet; 56 private boolean formatSet; 57 StreamReader()58 public StreamReader() { 59 oggPacket = new OggPacket(); 60 } 61 init(ExtractorOutput output, TrackOutput trackOutput)62 void init(ExtractorOutput output, TrackOutput trackOutput) { 63 this.extractorOutput = output; 64 this.trackOutput = trackOutput; 65 reset(true); 66 } 67 68 /** 69 * Resets the state of the {@link StreamReader}. 70 * 71 * @param headerData Resets parsed header data too. 72 */ reset(boolean headerData)73 protected void reset(boolean headerData) { 74 if (headerData) { 75 setupData = new SetupData(); 76 payloadStartPosition = 0; 77 state = STATE_READ_HEADERS; 78 } else { 79 state = STATE_SKIP_HEADERS; 80 } 81 targetGranule = -1; 82 currentGranule = 0; 83 } 84 85 /** 86 * @see Extractor#seek(long, long) 87 */ seek(long position, long timeUs)88 final void seek(long position, long timeUs) { 89 oggPacket.reset(); 90 if (position == 0) { 91 reset(!seekMapSet); 92 } else { 93 if (state != STATE_READ_HEADERS) { 94 targetGranule = convertTimeToGranule(timeUs); 95 oggSeeker.startSeek(targetGranule); 96 state = STATE_READ_PAYLOAD; 97 } 98 } 99 } 100 101 /** 102 * @see Extractor#read(ExtractorInput, PositionHolder) 103 */ read(ExtractorInput input, PositionHolder seekPosition)104 final int read(ExtractorInput input, PositionHolder seekPosition) 105 throws IOException, InterruptedException { 106 switch (state) { 107 case STATE_READ_HEADERS: 108 return readHeaders(input); 109 case STATE_SKIP_HEADERS: 110 input.skipFully((int) payloadStartPosition); 111 state = STATE_READ_PAYLOAD; 112 return Extractor.RESULT_CONTINUE; 113 case STATE_READ_PAYLOAD: 114 return readPayload(input, seekPosition); 115 default: 116 // Never happens. 117 throw new IllegalStateException(); 118 } 119 } 120 readHeaders(ExtractorInput input)121 private int readHeaders(ExtractorInput input) throws IOException, InterruptedException { 122 boolean readingHeaders = true; 123 while (readingHeaders) { 124 if (!oggPacket.populate(input)) { 125 state = STATE_END_OF_INPUT; 126 return Extractor.RESULT_END_OF_INPUT; 127 } 128 lengthOfReadPacket = input.getPosition() - payloadStartPosition; 129 130 readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData); 131 if (readingHeaders) { 132 payloadStartPosition = input.getPosition(); 133 } 134 } 135 136 sampleRate = setupData.format.sampleRate; 137 if (!formatSet) { 138 trackOutput.format(setupData.format); 139 formatSet = true; 140 } 141 142 if (setupData.oggSeeker != null) { 143 oggSeeker = setupData.oggSeeker; 144 } else if (input.getLength() == C.LENGTH_UNSET) { 145 oggSeeker = new UnseekableOggSeeker(); 146 } else { 147 OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader(); 148 boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. 149 oggSeeker = 150 new DefaultOggSeeker( 151 this, 152 payloadStartPosition, 153 input.getLength(), 154 firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, 155 firstPayloadPageHeader.granulePosition, 156 isLastPage); 157 } 158 159 setupData = null; 160 state = STATE_READ_PAYLOAD; 161 // First payload packet. Trim the payload array of the ogg packet after headers have been read. 162 oggPacket.trimPayload(); 163 return Extractor.RESULT_CONTINUE; 164 } 165 readPayload(ExtractorInput input, PositionHolder seekPosition)166 private int readPayload(ExtractorInput input, PositionHolder seekPosition) 167 throws IOException, InterruptedException { 168 long position = oggSeeker.read(input); 169 if (position >= 0) { 170 seekPosition.position = position; 171 return Extractor.RESULT_SEEK; 172 } else if (position < -1) { 173 onSeekEnd(-(position + 2)); 174 } 175 if (!seekMapSet) { 176 SeekMap seekMap = oggSeeker.createSeekMap(); 177 extractorOutput.seekMap(seekMap); 178 seekMapSet = true; 179 } 180 181 if (lengthOfReadPacket > 0 || oggPacket.populate(input)) { 182 lengthOfReadPacket = 0; 183 ParsableByteArray payload = oggPacket.getPayload(); 184 long granulesInPacket = preparePayload(payload); 185 if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) { 186 // calculate time and send payload data to codec 187 long timeUs = convertGranuleToTime(currentGranule); 188 trackOutput.sampleData(payload, payload.limit()); 189 trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null); 190 targetGranule = -1; 191 } 192 currentGranule += granulesInPacket; 193 } else { 194 state = STATE_END_OF_INPUT; 195 return Extractor.RESULT_END_OF_INPUT; 196 } 197 return Extractor.RESULT_CONTINUE; 198 } 199 200 /** 201 * Converts granule value to time. 202 * 203 * @param granule The granule value. 204 * @return Time in milliseconds. 205 */ convertGranuleToTime(long granule)206 protected long convertGranuleToTime(long granule) { 207 return (granule * C.MICROS_PER_SECOND) / sampleRate; 208 } 209 210 /** 211 * Converts time value to granule. 212 * 213 * @param timeUs Time in milliseconds. 214 * @return The granule value. 215 */ convertTimeToGranule(long timeUs)216 protected long convertTimeToGranule(long timeUs) { 217 return (sampleRate * timeUs) / C.MICROS_PER_SECOND; 218 } 219 220 /** 221 * Prepares payload data in the packet for submitting to TrackOutput and returns number of 222 * granules in the packet. 223 * 224 * @param packet Ogg payload data packet. 225 * @return Number of granules in the packet or -1 if the packet doesn't contain payload data. 226 */ preparePayload(ParsableByteArray packet)227 protected abstract long preparePayload(ParsableByteArray packet); 228 229 /** 230 * Checks if the given packet is a header packet and reads it. 231 * 232 * @param packet An ogg packet. 233 * @param position Position of the given header packet. 234 * @param setupData Setup data to be filled. 235 * @return Whether the packet contains header data. 236 */ readHeaders(ParsableByteArray packet, long position, SetupData setupData)237 protected abstract boolean readHeaders(ParsableByteArray packet, long position, 238 SetupData setupData) throws IOException, InterruptedException; 239 240 /** 241 * Called on end of seeking. 242 * 243 * @param currentGranule The granule at the current input position. 244 */ onSeekEnd(long currentGranule)245 protected void onSeekEnd(long currentGranule) { 246 this.currentGranule = currentGranule; 247 } 248 249 private static final class UnseekableOggSeeker implements OggSeeker { 250 251 @Override read(ExtractorInput input)252 public long read(ExtractorInput input) { 253 return -1; 254 } 255 256 @Override startSeek(long targetGranule)257 public void startSeek(long targetGranule) { 258 // Do nothing. 259 } 260 261 @Override createSeekMap()262 public SeekMap createSeekMap() { 263 return new SeekMap.Unseekable(C.TIME_UNSET); 264 } 265 266 } 267 268 } 269