mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
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:
parent
30ab3bef9a
commit
3674f9598e
11 changed files with 277 additions and 31 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
BIN
library/src/androidTest/assets/ogg/bear.opus
Normal file
BIN
library/src/androidTest/assets/ogg/bear.opus
Normal file
Binary file not shown.
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
|
|
|
|||
Loading…
Reference in a new issue