Commit 7d306ae5 by Oliver Woodman

Add language to MediaFormat + parse it from mdhd box.

+ Move conversion from framework -> exo format to FrameworkSampleSource.
+ Improve MediaFormat conversion test.
parent b2206866
...@@ -17,17 +17,20 @@ package com.google.android.exoplayer; ...@@ -17,17 +17,20 @@ package com.google.android.exoplayer;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
* Unit test for {@link MediaFormat}. * Unit test for {@link MediaFormat}.
*/ */
public class MediaFormatTest extends TestCase { public final class MediaFormatTest extends TestCase {
public void testConversionToFrameworkFormat() { public void testConversionToFrameworkFormat() {
if (Util.SDK_INT < 16) { if (Util.SDK_INT < 16) {
...@@ -41,20 +44,64 @@ public class MediaFormatTest extends TestCase { ...@@ -41,20 +44,64 @@ public class MediaFormatTest extends TestCase {
initData.add(initData1); initData.add(initData1);
initData.add(initData2); initData.add(initData2);
testConversionToFrameworkFormatV16(MediaFormat.createVideoFormat(
"video/xyz", 102400, 1000L, 1280, 720, 1, initData));
testConversionToFrameworkFormatV16(MediaFormat.createVideoFormat(
"video/xyz", MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, 1280, 720, 1, null));
testConversionToFrameworkFormatV16(MediaFormat.createAudioFormat(
"audio/xyz", 128, 1000L, 5, 44100, initData));
testConversionToFrameworkFormatV16(MediaFormat.createAudioFormat(
"audio/xyz", MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, 5, 44100, null));
testConversionToFrameworkFormatV16( testConversionToFrameworkFormatV16(
MediaFormat.createVideoFormat("video/xyz", 102400, 1000L, 1280, 720, 1.5f, initData)); MediaFormat.createTextFormat("text/xyz", "eng", 1000L));
testConversionToFrameworkFormatV16( testConversionToFrameworkFormatV16(
MediaFormat.createAudioFormat("audio/xyz", 102400, 1000L, 5, 44100, initData)); MediaFormat.createTextFormat("text/xyz", null, C.UNKNOWN_TIME_US));
} }
@SuppressLint("InlinedApi")
@TargetApi(16) @TargetApi(16)
private void testConversionToFrameworkFormatV16(MediaFormat format) { private static void testConversionToFrameworkFormatV16(MediaFormat in) {
// Convert to a framework MediaFormat and back again. android.media.MediaFormat out = in.getFrameworkMediaFormatV16();
MediaFormat convertedFormat = MediaFormat.createFromFrameworkMediaFormatV16( assertEquals(in.mimeType, out.getString(android.media.MediaFormat.KEY_MIME));
format.getFrameworkMediaFormatV16()); assertOptionalV16(out, android.media.MediaFormat.KEY_LANGUAGE, in.language);
// Assert that we end up with an equivalent object to the one we started with. assertOptionalV16(out, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, in.maxInputSize);
assertEquals(format.hashCode(), convertedFormat.hashCode()); assertOptionalV16(out, android.media.MediaFormat.KEY_WIDTH, in.width);
assertEquals(format, convertedFormat); assertOptionalV16(out, android.media.MediaFormat.KEY_HEIGHT, in.height);
assertOptionalV16(out, android.media.MediaFormat.KEY_CHANNEL_COUNT, in.channelCount);
assertOptionalV16(out, android.media.MediaFormat.KEY_SAMPLE_RATE, in.sampleRate);
assertOptionalV16(out, android.media.MediaFormat.KEY_MAX_WIDTH, in.getMaxVideoWidth());
assertOptionalV16(out, android.media.MediaFormat.KEY_MAX_HEIGHT, in.getMaxVideoHeight());
for (int i = 0; i < in.initializationData.size(); i++) {
byte[] originalData = in.initializationData.get(i);
ByteBuffer frameworkBuffer = out.getByteBuffer("csd-" + i);
byte[] frameworkData = Arrays.copyOf(frameworkBuffer.array(), frameworkBuffer.limit());
assertTrue(Arrays.equals(originalData, frameworkData));
}
if (in.durationUs == C.UNKNOWN_TIME_US) {
assertFalse(out.containsKey(android.media.MediaFormat.KEY_DURATION));
} else {
assertEquals(in.durationUs, out.getLong(android.media.MediaFormat.KEY_DURATION));
}
}
@TargetApi(16)
private static void assertOptionalV16(android.media.MediaFormat format, String key,
String value) {
if (value == null) {
assertFalse(format.containsKey(key));
} else {
assertEquals(value, format.getString(key));
}
}
@TargetApi(16)
private static void assertOptionalV16(android.media.MediaFormat format, String key,
int value) {
if (value == MediaFormat.NO_VALUE) {
assertFalse(format.containsKey(key));
} else {
assertEquals(value, format.getInteger(key));
}
} }
} }
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer.util.Assertions; ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.media.MediaExtractor; import android.media.MediaExtractor;
...@@ -30,6 +31,8 @@ import android.net.Uri; ...@@ -30,6 +31,8 @@ import android.net.Uri;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
...@@ -197,8 +200,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -197,8 +200,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
return NOTHING_READ; return NOTHING_READ;
} }
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16( formatHolder.format = createMediaFormat(extractor.getTrackFormat(track));
extractor.getTrackFormat(track));
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null; formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null;
trackStates[track] = TRACK_STATE_FORMAT_SENT; trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ; return FORMAT_READ;
...@@ -297,4 +299,37 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -297,4 +299,37 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
} }
} }
@SuppressLint("InlinedApi")
private static MediaFormat createMediaFormat(android.media.MediaFormat format) {
String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE);
int maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
int channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
ArrayList<byte[]> initializationData = new ArrayList<>();
for (int i = 0; format.containsKey("csd-" + i); i++) {
ByteBuffer buffer = format.getByteBuffer("csd-" + i);
byte[] data = new byte[buffer.limit()];
buffer.get(data);
initializationData.add(data);
buffer.flip();
}
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, MediaFormat.NO_VALUE,
channelCount, sampleRate, language, initializationData);
}
@TargetApi(16)
private static final String getOptionalStringV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getString(key) : null;
}
@TargetApi(16)
private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE;
}
} }
...@@ -890,8 +890,11 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -890,8 +890,11 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
* incorrectly on the host device. False otherwise. * incorrectly on the host device. False otherwise.
*/ */
private static boolean codecNeedsEndOfStreamWorkaround(String name) { private static boolean codecNeedsEndOfStreamWorkaround(String name) {
return Util.SDK_INT <= 17 && "ht7s3".equals(Util.DEVICE) // Tesco HUDL return Util.SDK_INT <= 17
&& "OMX.rk.video_decoder.avc".equals(name); && "OMX.rk.video_decoder.avc".equals(name)
&& ("ht7s3".equals(Util.DEVICE) // Tesco HUDL
|| "rk30sdk".equals(Util.DEVICE) // Rockchip rk30
|| "rk31sdk".equals(Util.DEVICE)); // Rockchip rk31
} }
} }
...@@ -70,15 +70,6 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { ...@@ -70,15 +70,6 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer {
*/ */
protected abstract boolean handlesTrack(TrackInfo trackInfo); protected abstract boolean handlesTrack(TrackInfo trackInfo);
/**
* Invoked when a track is selected.
*
* @param trackInfo The selected track.
*/
protected void onTrackSelected(TrackInfo trackInfo) {
// Do nothing.
}
@Override @Override
protected void onEnabled(int track, long positionUs, boolean joining) protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException { throws ExoPlaybackException {
......
...@@ -656,7 +656,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -656,7 +656,8 @@ public class DashChunkSource implements ChunkSource {
} }
return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL, return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL,
representation.format, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, representation.format, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment,
MediaFormat.createTextFormat(MimeTypes.TEXT_VTT), null, representationHolder.vttHeader); MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, representation.format.language), null,
representationHolder.vttHeader);
} else { } else {
return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format, return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format,
startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, sampleOffsetUs, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, sampleOffsetUs,
......
...@@ -62,10 +62,11 @@ import java.util.List; ...@@ -62,10 +62,11 @@ import java.util.List;
Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
.getContainerAtomOfType(Atom.TYPE_stbl); .getContainerAtomOfType(Atom.TYPE_stbl);
long mediaTimescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
StsdDataHolder stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs); StsdDataHolder stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs,
mdhdData.second);
return stsdData.mediaFormat == null ? null return stsdData.mediaFormat == null ? null
: new Track(id, trackType, mediaTimescale, durationUs, stsdData.mediaFormat, : new Track(id, trackType, mdhdData.first, durationUs, stsdData.mediaFormat,
stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength); stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength);
} }
...@@ -314,18 +315,25 @@ import java.util.List; ...@@ -314,18 +315,25 @@ import java.util.List;
* Parses an mdhd atom (defined in 14496-12). * Parses an mdhd atom (defined in 14496-12).
* *
* @param mdhd The mdhd atom to parse. * @param mdhd The mdhd atom to parse.
* @return The media timescale, defined as the number of time units that pass in one second. * @return A pair consisting of the media timescale defined as the number of time units that pass
* in one second, and the language code.
*/ */
private static long parseMdhd(ParsableByteArray mdhd) { private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {
mdhd.setPosition(Atom.HEADER_SIZE); mdhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = mdhd.readInt(); int fullAtom = mdhd.readInt();
int version = Atom.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
mdhd.skipBytes(version == 0 ? 8 : 16); mdhd.skipBytes(version == 0 ? 8 : 16);
return mdhd.readUnsignedInt(); long timescale = mdhd.readUnsignedInt();
mdhd.skipBytes(version == 0 ? 4 : 8);
int languageCode = mdhd.readUnsignedShort();
String language = "" + (char) (((languageCode >> 10) & 0x1F) + 0x60)
+ (char) (((languageCode >> 5) & 0x1F) + 0x60)
+ (char) (((languageCode) & 0x1F) + 0x60);
return Pair.create(timescale, language);
} }
private static StsdDataHolder parseStsd(ParsableByteArray stsd, long durationUs) { private static StsdDataHolder parseStsd(ParsableByteArray stsd, long durationUs,
String language) {
stsd.setPosition(Atom.FULL_HEADER_SIZE); stsd.setPosition(Atom.FULL_HEADER_SIZE);
int numberOfEntries = stsd.readInt(); int numberOfEntries = stsd.readInt();
StsdDataHolder holder = new StsdDataHolder(numberOfEntries); StsdDataHolder holder = new StsdDataHolder(numberOfEntries);
...@@ -344,9 +352,11 @@ import java.util.List; ...@@ -344,9 +352,11 @@ import java.util.List;
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs, parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs,
holder, i); holder, i);
} else if (childAtomType == Atom.TYPE_TTML) { } else if (childAtomType == Atom.TYPE_TTML) {
holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TTML, durationUs); holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TTML, language,
durationUs);
} else if (childAtomType == Atom.TYPE_tx3g) { } else if (childAtomType == Atom.TYPE_tx3g) {
holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TX3G, durationUs); holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TX3G, language,
durationUs);
} }
stsd.setPosition(childStartPosition + childAtomSize); stsd.setPosition(childStartPosition + childAtomSize);
} }
......
...@@ -35,7 +35,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -35,7 +35,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
public Id3Reader(TrackOutput output) { public Id3Reader(TrackOutput output) {
super(output); super(output);
output.format(MediaFormat.createTextFormat(MimeTypes.APPLICATION_ID3)); output.format(MediaFormat.createFormatForMimeType(MimeTypes.APPLICATION_ID3));
} }
@Override @Override
......
...@@ -32,7 +32,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -32,7 +32,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
public SeiReader(TrackOutput output) { public SeiReader(TrackOutput output) {
super(output); super(output);
output.format(MediaFormat.createTextFormat(MimeTypes.APPLICATION_EIA608)); output.format(MediaFormat.createTextFormat(MimeTypes.APPLICATION_EIA608, null));
} }
@Override @Override
......
...@@ -398,7 +398,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -398,7 +398,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
trackFormat.numChannels, trackFormat.audioSamplingRate, csd); trackFormat.numChannels, trackFormat.audioSamplingRate, csd);
return format; return format;
} else if (streamElement.type == StreamElement.TYPE_TEXT) { } else if (streamElement.type == StreamElement.TYPE_TEXT) {
return MediaFormat.createTextFormat(trackFormat.mimeType); return MediaFormat.createTextFormat(trackFormat.mimeType, trackFormat.language);
} }
return null; return null;
} }
......
...@@ -151,29 +151,14 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement ...@@ -151,29 +151,14 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
@Override @Override
protected boolean handlesTrack(TrackInfo trackInfo) { protected boolean handlesTrack(TrackInfo trackInfo) {
for (int i = 0; i < subtitleParsers.length; i++) { return getParserIndex(trackInfo) != -1;
if (subtitleParsers[i].canParse(trackInfo.mimeType)) {
return true;
}
}
return false;
}
@Override
protected void onTrackSelected(TrackInfo trackInfo) {
for (int i = 0; i < subtitleParsers.length; i++) {
if (subtitleParsers[i].canParse(trackInfo.mimeType)) {
parserIndex = i;
return;
}
}
throw new IllegalStateException("Invalid track selected");
} }
@Override @Override
protected void onEnabled(int track, long positionUs, boolean joining) protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException { throws ExoPlaybackException {
super.onEnabled(track, positionUs, joining); super.onEnabled(track, positionUs, joining);
parserIndex = getParserIndex(getTrackInfo(track));
parserThread = new HandlerThread("textParser"); parserThread = new HandlerThread("textParser");
parserThread.start(); parserThread.start();
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]); parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]);
...@@ -311,4 +296,13 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement ...@@ -311,4 +296,13 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
textRenderer.onCues(cues); textRenderer.onCues(cues);
} }
private int getParserIndex(TrackInfo trackInfo) {
for (int i = 0; i < subtitleParsers.length; i++) {
if (subtitleParsers[i].canParse(trackInfo.mimeType)) {
return i;
}
}
return -1;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment