Add support for reading AC-3 audio in HLS.

This commit is contained in:
Oliver Woodman 2015-05-01 20:10:47 +01:00
parent 9b6c30525e
commit aed45bb2ca
3 changed files with 309 additions and 2 deletions

View File

@ -0,0 +1,162 @@
/*
* Copyright (C) 2014 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.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.Ac3Util;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Parses a continuous AC-3 byte stream and extracts individual samples.
*/
/* package */ final class Ac3Reader extends ElementaryStreamReader {
private static final int STATE_FINDING_SYNC = 0;
private static final int STATE_READING_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2;
private static final int HEADER_SIZE = 8;
private final ParsableBitArray headerScratchBits;
private final ParsableByteArray headerScratchBytes;
private int state;
private int bytesRead;
// Used to find the header.
private boolean lastByteWas0B;
// Used when parsing the header.
private long frameDurationUs;
private MediaFormat mediaFormat;
private int sampleSize;
// Used when reading the samples.
private long timeUs;
public Ac3Reader(TrackOutput output) {
super(output);
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
headerScratchBytes = new ParsableByteArray(headerScratchBits.getData());
state = STATE_FINDING_SYNC;
}
public void seek() {
state = STATE_FINDING_SYNC;
bytesRead = 0;
lastByteWas0B = false;
}
@Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) {
timeUs = pesTimeUs;
}
while (data.bytesLeft() > 0) {
switch (state) {
case STATE_FINDING_SYNC:
if (skipToNextSync(data)) {
state = STATE_READING_HEADER;
headerScratchBytes.data[0] = 0x0B;
headerScratchBytes.data[1] = 0x77;
bytesRead = 2;
}
break;
case STATE_READING_HEADER:
if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
parseHeader();
headerScratchBytes.setPosition(0);
output.sampleData(headerScratchBytes, HEADER_SIZE);
state = STATE_READING_SAMPLE;
}
break;
case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
output.sampleData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
timeUs += frameDurationUs;
state = STATE_FINDING_SYNC;
}
break;
}
}
}
@Override
public void packetFinished() {
// Do nothing.
}
/**
* Continues a read from the provided {@code source} into a given {@code target}. It's assumed
* that the data should be written into {@code target} starting from an offset of zero.
*
* @param source The source from which to read.
* @param target The target into which data is to be read.
* @param targetLength The target length of the read.
* @return Whether the target length was reached.
*/
private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
source.readBytes(target, bytesRead, bytesToRead);
bytesRead += bytesToRead;
return bytesRead == targetLength;
}
/**
* Locates the next sync word, advancing the position to the byte that immediately follows it.
* If a sync word was not located, the position is advanced to the limit.
*
* @param pesBuffer The buffer whose position should be advanced.
* @return True if a sync word position was found. False otherwise.
*/
private boolean skipToNextSync(ParsableByteArray pesBuffer) {
while (pesBuffer.bytesLeft() > 0) {
if (!lastByteWas0B) {
lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B;
continue;
}
int secondByte = pesBuffer.readUnsignedByte();
if (secondByte == 0x77) {
lastByteWas0B = false;
return true;
} else {
lastByteWas0B = secondByte == 0x0B;
}
}
return false;
}
/**
* Parses the sample header.
*/
private void parseHeader() {
if (mediaFormat == null) {
headerScratchBits.setPosition(0);
mediaFormat = Ac3Util.parseFrameAc3Format(headerScratchBits);
output.format(mediaFormat);
}
headerScratchBits.setPosition(0);
sampleSize = Ac3Util.parseFrameSize(headerScratchBits);
frameDurationUs = (int) (1000000L * 8 * sampleSize / mediaFormat.bitrate);
}
}

View File

@ -40,6 +40,8 @@ public final class TsExtractor implements Extractor {
private static final int TS_PAT_PID = 0; private static final int TS_PAT_PID = 0;
private static final int TS_STREAM_TYPE_AAC = 0x0F; private static final int TS_STREAM_TYPE_AAC = 0x0F;
private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81;
private static final int TS_STREAM_TYPE_DVB_AC3 = 0x06;
private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1
@ -262,11 +264,14 @@ public final class TsExtractor implements Extractor {
case TS_STREAM_TYPE_AAC: case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC)); pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
break; break;
case TS_STREAM_TYPE_ATSC_AC3:
case TS_STREAM_TYPE_DVB_AC3:
pesPayloadReader = new Ac3Reader(output.track(streamType));
break;
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608)); SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608));
streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader); streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader);
seiReader);
break; break;
case TS_STREAM_TYPE_ID3: case TS_STREAM_TYPE_ID3:
pesPayloadReader = id3Reader; pesPayloadReader = id3Reader;

View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 2014 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.util;
import com.google.android.exoplayer.MediaFormat;
import java.util.Collections;
/**
* Utility methods for parsing AC-3 headers.
*/
public final class Ac3Util {
/** Sample rates, indexed by fscod. */
private static final int[] SAMPLE_RATES = new int[] {48000, 44100, 32000};
/** Channel counts, indexed by acmod. */
private static final int[] CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
/** Nominal bitrates in kbps, indexed by bit_rate_code. */
private static final int[] BITRATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192,
224, 256, 320, 384, 448, 512, 576, 640};
/** 16-bit words per sync frame, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) */
private static final int[] FRMSIZECOD_TO_FRAME_SIZE_44_1 = new int[] {69, 87, 104, 121, 139, 174,
208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};
/**
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to
* ETSI TS 102 366 Annex F.
*/
public static MediaFormat parseAnnexFAc3Format(ParsableByteArray data) {
// fscod (sample rate code)
int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
int sampleRate = SAMPLE_RATES[fscod];
int nextByte = data.readUnsignedByte();
// Map acmod (audio coding mode) onto a channel count.
int channelCount = CHANNEL_COUNTS[(nextByte & 0x38) >> 3];
// lfeon (low frequency effects on)
if ((nextByte & 0x04) != 0) {
channelCount++;
}
// Map bit_rate_code onto a bitrate in bit/s.
int bitrate = BITRATES[((nextByte & 0x03) << 3) + (data.readUnsignedByte() >> 5)] * 1000;
return MediaFormat.createAudioFormat(MimeTypes.AUDIO_AC3, MediaFormat.NO_VALUE,
MediaFormat.NO_VALUE, channelCount, sampleRate, bitrate, Collections.<byte[]>emptyList());
}
/**
* Returns the AC-3 format given {@code data} containing the EC3SpecificBox according to
* ETSI TS 102 366 Annex F.
*/
public static MediaFormat parseAnnexFEAc3Format(ParsableByteArray data) {
data.skipBytes(2); // Skip data_rate and num_ind_sub.
// Read only the first substream.
// TODO: Read later substreams?
// fscod (sample rate code)
int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
int sampleRate = SAMPLE_RATES[fscod];
int nextByte = data.readUnsignedByte();
// Map acmod (audio coding mode) onto a channel count.
int channelCount = CHANNEL_COUNTS[(nextByte & 0x0E) >> 1];
// lfeon (low frequency effects on)
if ((nextByte & 0x01) != 0) {
channelCount++;
}
return MediaFormat.createAudioFormat(MimeTypes.AUDIO_EC3, MediaFormat.NO_VALUE,
channelCount, sampleRate, Collections.<byte[]>emptyList());
}
/**
* Returns the AC-3 format given {@code data} containing the frame header starting from the sync
* word.
*
* @param data Data to parse, positioned at the start of the syncword.
* @return AC-3 format parsed from data in the header.
*/
public static MediaFormat parseFrameAc3Format(ParsableBitArray data) {
// Skip syncword and crc1.
data.skipBits(4 * 8);
int fscod = data.readBits(2);
int frmsizecod = data.readBits(6);
data.skipBits(8); // bsid (5 bits) + bsmod (3 bits)
int acmod = data.readBits(3);
if ((acmod & 0x01) != 0 && acmod != 1) {
data.skipBits(2); // cmixlev
}
if ((acmod & 0x04) != 0) {
data.skipBits(2); // surmixlev
}
if (acmod == 0x02) {
data.skipBits(2); // dsurmod
}
boolean lfeon = data.readBit();
return MediaFormat.createAudioFormat(MimeTypes.AUDIO_AC3, MediaFormat.NO_VALUE,
MediaFormat.NO_VALUE, CHANNEL_COUNTS[acmod] + (lfeon ? 1 : 0), SAMPLE_RATES[fscod],
BITRATES[frmsizecod / 2] * 1000, Collections.<byte[]>emptyList());
}
/**
* Returns the AC-3 frame size in bytes given {@code data} containing the frame header starting
* from the sync word.
*
* @param data Data to parse, positioned at the start of the syncword.
* @return The frame size parsed from data in the header.
*/
public static int parseFrameSize(ParsableBitArray data) {
// Skip syncword and crc1.
data.skipBits(4 * 8);
int fscod = data.readBits(2);
int frmsizecod = data.readBits(6);
int sampleRate = SAMPLE_RATES[fscod];
int bitrate = BITRATES[frmsizecod / 2];
if (sampleRate == 32000) {
return 6 * bitrate;
} else if (sampleRate == 44100) {
return 2 * (FRMSIZECOD_TO_FRAME_SIZE_44_1[frmsizecod / 2] + (frmsizecod % 2));
} else { // sampleRate == 48000
return 4 * bitrate;
}
}
private Ac3Util() {
// Prevent instantiation.
}
}