diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java index 4a609e301c..a13927969d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java @@ -44,6 +44,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.UUID; /** * Extracts data from a Matroska or WebM file. @@ -64,6 +65,7 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP"; private static final String CODEC_ID_H264 = "V_MPEG4/ISO/AVC"; private static final String CODEC_ID_H265 = "V_MPEGH/ISO/HEVC"; + private static final String CODEC_ID_FOURCC = "V_MS/VFW/FOURCC"; private static final String CODEC_ID_VORBIS = "A_VORBIS"; private static final String CODEC_ID_OPUS = "A_OPUS"; private static final String CODEC_ID_AAC = "A_AAC"; @@ -75,6 +77,8 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS"; private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS"; private static final String CODEC_ID_FLAC = "A_FLAC"; + private static final String CODEC_ID_ACM = "A_MS/ACM"; + private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT"; private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; @@ -123,6 +127,7 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_DISPLAY_UNIT = 0x54B2; private static final int ID_AUDIO = 0xE1; private static final int ID_CHANNELS = 0x9F; + private static final int ID_AUDIO_BIT_DEPTH = 0x6264; private static final int ID_SAMPLING_FREQUENCY = 0xB5; private static final int ID_CONTENT_ENCODINGS = 0x6D80; private static final int ID_CONTENT_ENCODING = 0x6240; @@ -148,6 +153,8 @@ public final class MatroskaExtractor implements Extractor { private static final int LACING_FIXED_SIZE = 2; private static final int LACING_EBML = 3; + private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; + /** * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be @@ -174,6 +181,23 @@ public final class MatroskaExtractor implements Extractor { */ private static final int SUBRIP_TIMECODE_LENGTH = 12; + /** + * The length in bytes of a WAVEFORMATEX structure. + */ + private static final int WAVE_FORMAT_SIZE = 18; + /** + * Format tag indicating a WAVEFORMATEXTENSIBLE structure. + */ + private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + /** + * Format tag for PCM. + */ + private static final int WAVE_FORMAT_PCM = 1; + /** + * Sub format for PCM. + */ + private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L); + private final EbmlReader reader; private final VarintReader varintReader; private final SparseArray tracks; @@ -330,6 +354,7 @@ public final class MatroskaExtractor implements Extractor { case ID_CODEC_DELAY: case ID_SEEK_PRE_ROLL: case ID_CHANNELS: + case ID_AUDIO_BIT_DEPTH: case ID_CONTENT_ENCODING_ORDER: case ID_CONTENT_ENCODING_SCOPE: case ID_CONTENT_COMPRESSION_ALGORITHM: @@ -535,7 +560,7 @@ public final class MatroskaExtractor implements Extractor { return; case ID_DEFAULT_DURATION: currentTrack.defaultSampleDurationNs = (int) value; - break; + return; case ID_CODEC_DELAY: currentTrack.codecDelayNs = value; return; @@ -545,6 +570,9 @@ public final class MatroskaExtractor implements Extractor { case ID_CHANNELS: currentTrack.channelCount = (int) value; return; + case ID_AUDIO_BIT_DEPTH: + currentTrack.audioBitDepth = (int) value; + return; case ID_REFERENCE_BLOCK: sampleSeenReferenceBlock = true; return; @@ -1058,6 +1086,7 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_MPEG4_AP.equals(codecId) || CODEC_ID_H264.equals(codecId) || CODEC_ID_H265.equals(codecId) + || CODEC_ID_FOURCC.equals(codecId) || CODEC_ID_OPUS.equals(codecId) || CODEC_ID_VORBIS.equals(codecId) || CODEC_ID_AAC.equals(codecId) @@ -1069,6 +1098,8 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_DTS_EXPRESS.equals(codecId) || CODEC_ID_DTS_LOSSLESS.equals(codecId) || CODEC_ID_FLAC.equals(codecId) + || CODEC_ID_ACM.equals(codecId) + || CODEC_ID_PCM_INT_LIT.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) || CODEC_ID_PGS.equals(codecId); @@ -1161,6 +1192,7 @@ public final class MatroskaExtractor implements Extractor { // Audio elements. Initially set to their default values. public int channelCount = 1; + public int audioBitDepth = -1; public int sampleRate = 8000; public long codecDelayNs = 0; public long seekPreRollNs = 0; @@ -1210,6 +1242,10 @@ public final class MatroskaExtractor implements Extractor { initializationData = hevcData.first; nalUnitLengthFieldLength = hevcData.second; break; + case CODEC_ID_FOURCC: + mimeType = MimeTypes.VIDEO_VC1; + initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); + break; case CODEC_ID_VORBIS: mimeType = MimeTypes.AUDIO_VORBIS; maxInputSize = VORBIS_MAX_INPUT_SIZE; @@ -1253,6 +1289,21 @@ public final class MatroskaExtractor implements Extractor { mimeType = MimeTypes.AUDIO_FLAC; initializationData = Collections.singletonList(codecPrivate); break; + case CODEC_ID_ACM: + mimeType = MimeTypes.AUDIO_RAW; + if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { + throw new ParserException("Non-PCM MS/ACM is unsupported"); + } + if (audioBitDepth != 16) { + throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + } + break; + case CODEC_ID_PCM_INT_LIT: + mimeType = MimeTypes.AUDIO_RAW; + if (audioBitDepth != 16) { + throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + } + break; case CODEC_ID_SUBRIP: mimeType = MimeTypes.APPLICATION_SUBRIP; break; @@ -1300,6 +1351,42 @@ public final class MatroskaExtractor implements Extractor { this.output.format(format); } + /** + * Builds initialization data for a {@link Format} from FourCC codec private data. + *

+ * VC1 is the only supported compression type. + * + * @return The initialization data for the {@link Format}. + * @throws ParserException If the initialization data could not be built. + */ + private static List parseFourCcVc1Private(ParsableByteArray buffer) + throws ParserException { + try { + buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). + long compression = buffer.readLittleEndianUnsignedInt(); + if (compression != FOURCC_COMPRESSION_VC1) { + throw new ParserException("Unsupported FourCC compression type: " + compression); + } + + // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 + // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). + int startOffset = buffer.getPosition() + 20; + byte[] bufferData = buffer.data; + for (int offset = startOffset; offset < bufferData.length - 4; offset++) { + if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { + // We've found the initialization data. + byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); + return Collections.singletonList(initializationData); + } + } + + throw new ParserException("Failed to find FourCC VC1 initialization data"); + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing FourCC VC1 codec private"); + } + } + /** * Builds initialization data for a {@link Format} from H.264 (AVC) codec private data. * @@ -1434,6 +1521,29 @@ public final class MatroskaExtractor implements Extractor { } } + /** + * Parses an MS/ACM codec private, returning whether it indicates PCM audio. + * + * @return True if the codec private indicates PCM audio. False otherwise. + * @throws ParserException If a parsing error occurs. + */ + private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException { + try { + int formatTag = buffer.readLittleEndianUnsignedShort(); + if (formatTag == WAVE_FORMAT_PCM) { + return true; + } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) { + buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4) + return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits() + && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits(); + } else { + return false; + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing MS/ACM codec private"); + } + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 5cee668dae..45d3e46b58 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -34,6 +34,7 @@ public final class MimeTypes { public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; + public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";