ogg't'opus reader

Support for opus content in ogg container.
TODO: Sample duration and seeking.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=121940392
This commit is contained in:
eguven 2016-05-10 06:24:59 -07:00 committed by Oliver Woodman
parent 30ab3bef9a
commit 3674f9598e
11 changed files with 277 additions and 31 deletions

View file

@ -76,8 +76,7 @@ public class SampleChooserActivity extends Activity {
group.addAll(Samples.MISC);
sampleGroups.add(group);
group = new SampleGroup("Extensions");
group.addAll(Samples.VP9_EXTENSION_SAMPLES);
group.addAll(Samples.VP9_OPUS_EXTENSION_SAMPLES);
group.addAll(Samples.EXTENSION);
sampleGroups.add(group);
ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list);

View file

@ -262,16 +262,13 @@ import java.util.Locale;
"http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0", Util.TYPE_OTHER),
};
public static final Sample[] VP9_EXTENSION_SAMPLES = new Sample[] {
public static final Sample[] EXTENSION = new Sample[] {
new Sample("Google Glass DASH - VP9 Only",
"http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9.mpd",
Util.TYPE_DASH, true),
new Sample("Google Glass DASH - VP9 and Vorbis",
"http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_vorbis.mpd",
Util.TYPE_DASH, true),
};
public static final Sample[] VP9_OPUS_EXTENSION_SAMPLES = new Sample[] {
new Sample("Google Glass DASH - VP9 and Opus",
"http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd",
Util.TYPE_DASH, true),

Binary file not shown.

View file

@ -38,17 +38,17 @@ public final class OggExtractorTest extends TestCase {
public void testSniffVorbis() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
TestData.buildOggHeader(0x02, 0, 1000, 1),
TestUtil.createByteArray(7), // Laces
new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'});
assertTrue(sniff(createInput(data)));
}
public void testSniffFlac() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
new byte[]{0x7F, 'F', 'L', 'A', 'C', ' ', ' '});
TestData.buildOggHeader(0x02, 0, 1000, 1),
TestUtil.createByteArray(5), // Laces
new byte[]{0x7F, 'F', 'L', 'A', 'C'});
assertTrue(sniff(createInput(data)));
}
@ -66,8 +66,8 @@ public final class OggExtractorTest extends TestCase {
public void testSniffInvalidHeader() throws Exception {
byte[] data = TestUtil.joinByteArrays(
TestData.buildOggHeader(0x02, 0, 1000, 0x02),
TestUtil.createByteArray(120, 120), // Laces
TestData.buildOggHeader(0x02, 0, 1000, 1),
TestUtil.createByteArray(7), // Laces
new byte[]{0x7F, 'X', 'o', 'r', 'b', 'i', 's'});
assertFalse(sniff(createInput(data)));
}

View file

@ -0,0 +1,107 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.testutil.FakeExtractorOutput;
import com.google.android.exoplayer.testutil.FakeTrackOutput;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.DefaultDataSource;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.content.Context;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import java.io.IOException;
/**
* Unit test for {@link OpusReader}.
*/
public final class OpusReaderTest extends InstrumentationTestCase {
private static final String TEST_FILE = "asset:///ogg/bear.opus";
private OggExtractor extractor;
private OpusReader opusReader;
private FakeExtractorOutput extractorOutput;
private DefaultExtractorInput extractorInput;
@Override
public void setUp() throws Exception {
super.setUp();
Context context = getInstrumentation().getContext();
DataSource dataSource = new DefaultDataSource(context, null, Util
.getUserAgent(context, "ExoPlayerExtFlacTest"), false);
Uri uri = Uri.parse(TEST_FILE);
long length = dataSource.open(new DataSpec(uri, 0, C.LENGTH_UNBOUNDED, null));
extractorInput = new DefaultExtractorInput(dataSource, 0, length);
extractor = new OggExtractor();
assertTrue(extractor.sniff(extractorInput));
extractorInput.resetPeekPosition();
opusReader = (OpusReader) extractor.getStreamReader();
extractorOutput = new FakeExtractorOutput();
extractor.init(extractorOutput);
}
public void testSniffOpus() throws Exception {
// Do nothing. All assertions are in setUp()
}
public void testParseHeader() throws Exception {
FakeTrackOutput trackOutput = parseFile(false);
trackOutput.assertSampleCount(0);
Format format = trackOutput.format;
assertNotNull(format);
assertEquals(MimeTypes.AUDIO_OPUS, format.sampleMimeType);
assertEquals(48000, format.sampleRate);
assertEquals(2, format.channelCount);
}
public void testParseWholeFile() throws Exception {
FakeTrackOutput trackOutput = parseFile(true);
trackOutput.assertSampleCount(275);
}
private FakeTrackOutput parseFile(boolean parseAll) throws IOException, InterruptedException {
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
do {
readResult = extractor.read(extractorInput, seekPositionHolder);
if (readResult == Extractor.RESULT_SEEK) {
fail("There should be no seek");
}
} while (readResult != Extractor.RESULT_END_OF_INPUT && parseAll);
assertEquals(1, extractorOutput.trackOutputs.size());
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertNotNull(trackOutput);
return trackOutput;
}
}

View file

@ -46,8 +46,8 @@ import java.util.List;
private boolean firstAudioPacketProcessed;
/* package */ static boolean verifyBitstreamType(ParsableByteArray data) {
return data.readUnsignedByte() == 0x7F && // packet type
public static boolean verifyBitstreamType(ParsableByteArray data) {
return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
}

View file

@ -38,20 +38,19 @@ public class OggExtractor implements Extractor {
ParsableByteArray scratch = new ParsableByteArray(new byte[OggUtil.PAGE_HEADER_SIZE], 0);
OggUtil.PageHeader header = new OggUtil.PageHeader();
if (!OggUtil.populatePageHeader(input, header, scratch, true)
|| (header.type & 0x02) != 0x02 || header.bodySize < 7) {
|| (header.type & 0x02) != 0x02) {
return false;
}
scratch.reset();
input.peekFully(scratch.data, 0, 7);
if (FlacReader.verifyBitstreamType(scratch)) {
input.peekFully(scratch.data, 0, header.bodySize);
scratch.setLimit(header.bodySize);
if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
streamReader = new FlacReader();
} else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
streamReader = new VorbisReader();
} else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
streamReader = new OpusReader();
} else {
scratch.reset();
if (VorbisReader.verifyBitstreamType(scratch)) {
streamReader = new VorbisReader();
} else {
return false;
}
return false;
}
return true;
} catch (ParserException e) {
@ -83,4 +82,15 @@ public class OggExtractor implements Extractor {
throws IOException, InterruptedException {
return streamReader.read(input, seekPosition);
}
//@VisibleForTesting
/* package */ StreamReader getStreamReader() {
return streamReader;
}
private static ParsableByteArray resetPosition(ParsableByteArray scratch) {
scratch.setPosition(0);
return scratch;
}
}

View file

@ -87,7 +87,7 @@ import java.io.IOException;
* as argument.
*
* @param input the {@link ExtractorInput} to read from.
* @param header the {@link PageHeader} to read from.
* @param header the {@link PageHeader} to be populated.
* @param scratch a scratch array temporary use. Its size should be at least PAGE_HEADER_SIZE
* @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something
* goes wrong.

View file

@ -0,0 +1,125 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* {@link StreamReader} to extract Opus data out of Ogg byte stream.
*/
/* package */ final class OpusReader extends StreamReader {
/**
* Opus streams are always decoded at 48000 Hz.
*/
private static final int SAMPLE_RATE = 48000;
private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private static final int STATE_READ_HEADER = 0;
private static final int STATE_READ_TAGS = 1;
private static final int STATE_READ_AUDIO = 2;
private int state = STATE_READ_HEADER;
private long timeUs;
public static boolean verifyBitstreamType(ParsableByteArray data) {
if (data.bytesLeft() < OPUS_SIGNATURE.length) {
return false;
}
byte[] header = new byte[OPUS_SIGNATURE.length];
data.readBytes(header, 0, OPUS_SIGNATURE.length);
return Arrays.equals(header, OPUS_SIGNATURE);
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (!oggParser.readPacket(input, scratch)) {
return Extractor.RESULT_END_OF_INPUT;
}
byte[] data = scratch.data;
int dataSize = scratch.limit();
switch (state) {
case STATE_READ_HEADER: {
byte[] metadata = Arrays.copyOfRange(data, 0, dataSize);
int channelCount = metadata[9] & 0xFF;
List<byte[]> initializationData = Collections.singletonList(metadata);
trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS,
Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE,
initializationData, null, null));
state = STATE_READ_TAGS;
} break;
case STATE_READ_TAGS:
// skip this packet
state = STATE_READ_AUDIO;
extractorOutput.seekMap(new SeekMap.Unseekable(C.UNSET_TIME_US));
break;
case STATE_READ_AUDIO:
trackOutput.sampleData(scratch, dataSize);
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, dataSize, 0, null);
timeUs += getPacketDuration(data);
break;
}
scratch.reset();
return Extractor.RESULT_CONTINUE;
}
private long getPacketDuration(byte[] packet) {
int toc = packet[0] & 0xFF;
int frames;
switch (toc & 0x3) {
case 0:
frames = 1;
break;
case 1:
case 2:
frames = 2;
break;
default:
frames = packet[1] & 0x3F;
break;
}
int config = toc >> 3;
int length = config & 0x3;
if (config >= 16) {
length = 2500 << length;
} else if (config >= 12) {
length = 10000 << (length & 0x1);
} else if (length == 3) {
length = 60000;
} else {
length = 10000 << length;
}
return frames * length;
}
}

View file

@ -51,7 +51,7 @@ import java.util.ArrayList;
private long totalSamples;
private long durationUs;
/* package */ static boolean verifyBitstreamType(ParsableByteArray data) {
public static boolean verifyBitstreamType(ParsableByteArray data) {
try {
return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
} catch (ParserException e) {

View file

@ -121,15 +121,23 @@ import java.util.Arrays;
*
* @param headerType the type of the header expected.
* @param header the alleged header bytes.
* @param quite if {@code true} no exceptions are thrown. Instead {@code false} is returned.
* @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned.
* @return the number of bytes read.
* @throws ParserException thrown if header type or capture pattern is not as expected.
*/
public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header,
boolean quite)
boolean quiet)
throws ParserException {
if (header.bytesLeft() < 7) {
if (quiet) {
return false;
} else {
throw new ParserException("too short header: " + header.bytesLeft());
}
}
if (header.readUnsignedByte() != headerType) {
if (quite) {
if (quiet) {
return false;
} else {
throw new ParserException("expected header type " + Integer.toHexString(headerType));
@ -142,7 +150,7 @@ import java.util.Arrays;
&& header.readUnsignedByte() == 'b'
&& header.readUnsignedByte() == 'i'
&& header.readUnsignedByte() == 's')) {
if (quite) {
if (quiet) {
return false;
} else {
throw new ParserException("expected characters 'vorbis'");