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