Add ID3 Timed Metadata support for HLS #67

This commit is contained in:
Andrey Udovenko 2014-10-28 13:24:12 -04:00
parent ca31010028
commit d3a05c9a44
11 changed files with 546 additions and 5 deletions

View File

@ -25,6 +25,7 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.metadata.Metadata;
import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView; import com.google.android.exoplayer.text.SubtitleView;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -38,6 +39,7 @@ import android.graphics.Point;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.view.Display; import android.view.Display;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -54,11 +56,15 @@ import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.TextView; import android.widget.TextView;
import java.util.List;
/** /**
* An activity that plays media using {@link DemoPlayer}. * An activity that plays media using {@link DemoPlayer}.
*/ */
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
DemoPlayer.Listener, DemoPlayer.TextListener { DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.MetadataListener {
private static final String TAG = "FullPlayerActivity";
private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f;
private static final int MENU_GROUP_TRACKS = 1; private static final int MENU_GROUP_TRACKS = 1;
@ -187,6 +193,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
player = new DemoPlayer(getRendererBuilder()); player = new DemoPlayer(getRendererBuilder());
player.addListener(this); player.addListener(this);
player.setTextListener(this); player.setTextListener(this);
player.setMetadataListener(this);
player.seekTo(playerPosition); player.seekTo(playerPosition);
playerNeedsPrepare = true; playerNeedsPrepare = true;
mediaController.setMediaPlayer(player.getPlayerControl()); mediaController.setMediaPlayer(player.getPlayerControl());
@ -403,6 +410,16 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
} }
} }
// DemoPlayer.MetadataListener implementation
@Override
public void onMetadata(List<Metadata> metadata) {
for (int i = 0; i < metadata.size(); i++) {
Metadata next = metadata.get(i);
Log.i(TAG, "ID3 TimedMetadata: key=" + next.key + ", value=" + next.value);
}
}
// SurfaceHolder.Callback implementation // SurfaceHolder.Callback implementation
@Override @Override

View File

@ -26,6 +26,8 @@ import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.metadata.Metadata;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.util.PlayerControl; import com.google.android.exoplayer.util.PlayerControl;
@ -36,6 +38,7 @@ import android.os.Looper;
import android.view.Surface; import android.view.Surface;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
/** /**
@ -46,7 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer,
StreamingDrmSessionManager.EventListener { MetadataTrackRenderer.MetadataRenderer, StreamingDrmSessionManager.EventListener {
/** /**
* Builds renderers for the player. * Builds renderers for the player.
@ -134,6 +137,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onText(String text); void onText(String text);
} }
/**
* A listener for receiving metadata parsed from the media stream.
*/
public interface MetadataListener {
void onMetadata(List<Metadata> metadata);
}
// Constants pulled into this class for convenience. // Constants pulled into this class for convenience.
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
@ -144,11 +154,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public static final int DISABLED_TRACK = -1; public static final int DISABLED_TRACK = -1;
public static final int PRIMARY_TRACK = 0; public static final int PRIMARY_TRACK = 0;
public static final int RENDERER_COUNT = 4; public static final int RENDERER_COUNT = 5;
public static final int TYPE_VIDEO = 0; public static final int TYPE_VIDEO = 0;
public static final int TYPE_AUDIO = 1; public static final int TYPE_AUDIO = 1;
public static final int TYPE_TEXT = 2; public static final int TYPE_TEXT = 2;
public static final int TYPE_DEBUG = 3; public static final int TYPE_TIMED_METADATA = 3;
public static final int TYPE_DEBUG = 4;
private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int RENDERER_BUILDING_STATE_IDLE = 1;
private static final int RENDERER_BUILDING_STATE_BUILDING = 2; private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
@ -173,6 +184,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private int[] selectedTracks; private int[] selectedTracks;
private TextListener textListener; private TextListener textListener;
private MetadataListener metadataListener;
private InternalErrorListener internalErrorListener; private InternalErrorListener internalErrorListener;
private InfoListener infoListener; private InfoListener infoListener;
@ -214,6 +226,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
textListener = listener; textListener = listener;
} }
public void setMetadataListener(MetadataListener listener) {
metadataListener = listener;
}
public void setSurface(Surface surface) { public void setSurface(Surface surface) {
this.surface = surface; this.surface = surface;
pushSurfaceAndVideoTrack(false); pushSurfaceAndVideoTrack(false);
@ -458,6 +474,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
} }
@Override
public void onMetadata(List<Metadata> metadata) {
if (metadataListener != null) {
metadataListener.onMetadata(metadata);
}
}
@Override @Override
public void onPlayWhenReadyCommitted() { public void onPlayWhenReadyCommitted() {
// Do nothing. // Do nothing.

View File

@ -28,6 +28,8 @@ import com.google.android.exoplayer.hls.HlsMasterPlaylist;
import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsMasterPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.Id3Parser;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.upstream.UriDataSource;
@ -97,9 +99,13 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
MetadataTrackRenderer metadataRenderer = new MetadataTrackRenderer(sampleSource,
new Id3Parser(), player, player.getMainHandler().getLooper());
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TIMED_METADATA] = metadataRenderer;
callback.onRenderers(null, null, renderers); callback.onRenderers(null, null, renderers);
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
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.SuppressLint;
@ -78,6 +79,11 @@ public class MediaFormat {
sampleRate, initializationData); sampleRate, initializationData);
} }
public static MediaFormat createId3Format() {
return new MediaFormat(MimeTypes.APPLICATION_ID3, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, null);
}
@TargetApi(16) @TargetApi(16)
private MediaFormat(android.media.MediaFormat format) { private MediaFormat(android.media.MediaFormat format) {
this.frameworkMediaFormat = format; this.frameworkMediaFormat = format;

View File

@ -0,0 +1,156 @@
/*
* 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.metadata;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.parser.ts.BitsArray;
import com.google.android.exoplayer.util.MimeTypes;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Extracts individual TXXX text frames from raw ID3 data.
*/
public class Id3Parser implements MetadataParser {
@Override
public boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_ID3);
}
@Override
public List<Metadata> parse(byte[] data, int size)
throws UnsupportedEncodingException, ParserException {
BitsArray id3Buffer = new BitsArray(data, size);
int id3Size = parseId3Header(id3Buffer);
List<Metadata> metadata = new ArrayList<Metadata>();
while (id3Size > 0) {
int frameId0 = id3Buffer.readUnsignedByte();
int frameId1 = id3Buffer.readUnsignedByte();
int frameId2 = id3Buffer.readUnsignedByte();
int frameId3 = id3Buffer.readUnsignedByte();
int frameSize = id3Buffer.readSynchSafeInt();
if (frameSize <= 1) {
break;
}
id3Buffer.skipBytes(2); // Skip frame flags.
// Check Frame ID == TXXX.
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
int encoding = id3Buffer.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1];
id3Buffer.readBytes(frame, 0, frameSize - 1);
int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String key = new String(frame, 0, firstZeroIndex, charset);
int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0);
int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex,
charset);
metadata.add(new Metadata(key, value));
} else {
id3Buffer.skipBytes(frameSize);
}
id3Size -= frameSize + 10 /* header size */;
}
return Collections.unmodifiableList(metadata);
}
private static int indexOf(byte[] data, int fromIndex, byte key) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] == key) {
return i;
}
}
return data.length;
}
private static int indexOfNot(byte[] data, int fromIndex, byte key) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] != key) {
return i;
}
}
return data.length;
}
/**
* Parses ID3 header.
* @param id3Buffer A {@link BitsArray} with raw ID3 data.
* @return The size of data that contains ID3 frames without header and footer.
* @throws ParserException If ID3 file identifier != "ID3".
*/
private static int parseId3Header(BitsArray id3Buffer) throws ParserException {
int id1 = id3Buffer.readUnsignedByte();
int id2 = id3Buffer.readUnsignedByte();
int id3 = id3Buffer.readUnsignedByte();
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
throw new ParserException(String.format(
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
}
id3Buffer.skipBytes(2); // Skip version.
int flags = id3Buffer.readUnsignedByte();
int id3Size = id3Buffer.readSynchSafeInt();
// Check if extended header presents.
if ((flags & 0x2) != 0) {
int extendedHeaderSize = id3Buffer.readSynchSafeInt();
if (extendedHeaderSize > 4) {
id3Buffer.skipBytes(extendedHeaderSize - 4);
}
id3Size -= extendedHeaderSize;
}
// Check if footer presents.
if ((flags & 0x8) != 0) {
id3Size -= 10;
}
return id3Size;
}
/**
* Maps encoding byte from ID3v2 frame to a Charset.
* @param encodingByte The value of encoding byte from ID3v2 frame.
* @return Charset name.
*/
private static String getCharsetName(int encodingByte) {
switch (encodingByte) {
case 0:
return "ISO-8859-1";
case 1:
return "UTF-16";
case 2:
return "UTF-16BE";
case 3:
return "UTF-8";
default:
return "ISO-8859-1";
}
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.metadata;
/**
* A metadata that contains textual data associated with time indices.
*/
public class Metadata {
public final String key;
public final String value;
public Metadata(String key, String value) {
this.key = key;
this.value = value;
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.metadata;
import java.io.IOException;
import java.util.List;
/**
* Parses {@link Metadata}s from binary data.
*/
public interface MetadataParser {
/**
* Checks whether the parser supports a given mime type.
*
* @param mimeType A subtitle mime type.
* @return Whether the mime type is supported.
*/
public boolean canParse(String mimeType);
/**
* Parses a list of {@link Metadata} objects from the provided binary data.
*
* @param data The raw binary data from which to parse the metadata.
* @param size The size of the input data.
* @return A parsed {@link List} of {@link Metadata} objects.
* @throws IOException If a problem occurred parsing the data.
*/
public List<Metadata> parse(byte[] data, int size)
throws IOException;
}

View File

@ -0,0 +1,211 @@
/*
* 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.metadata;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.util.Assertions;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
import java.util.List;
/**
* A {@link TrackRenderer} for metadata embedded in a media stream.
*/
public class MetadataTrackRenderer extends TrackRenderer implements Callback {
/**
* An interface for components that process metadata.
*/
public interface MetadataRenderer {
/**
* Invoked each time there is a metadata associated with current playback time.
*
* @param metadata The metadata to process.
*/
void onMetadata(List<Metadata> metadata);
}
private static final int MSG_INVOKE_RENDERER = 0;
private final SampleSource source;
private final MetadataParser metadataParser;
private final MetadataRenderer metadataRenderer;
private final Handler metadataHandler;
private final MediaFormatHolder formatHolder;
private final SampleHolder sampleHolder;
private int trackIndex;
private long currentPositionUs;
private boolean inputStreamEnded;
private long pendingMetadataTimestamp;
private List<Metadata> pendingMetadata;
/**
* @param source A source from which samples containing metadata can be read.
* @param metadataParser A parser for parsing the metadata.
* @param metadataRenderer The metadata renderer to receive the parsed metadata.
* @param metadataRendererLooper The looper associated with the thread on which metadataRenderer
* should be invoked. If the renderer makes use of standard Android UI components, then this
* should normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
*/
public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser,
MetadataRenderer metadataRenderer, Looper metadataRendererLooper) {
this.source = Assertions.checkNotNull(source);
this.metadataParser = Assertions.checkNotNull(metadataParser);
this.metadataRenderer = Assertions.checkNotNull(metadataRenderer);
this.metadataHandler = metadataRendererLooper == null ? null
: new Handler(metadataRendererLooper, this);
formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@Override
protected int doPrepare() throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
for (int i = 0; i < source.getTrackCount(); i++) {
if (metadataParser.canParse(source.getTrackInfo(i).mimeType)) {
trackIndex = i;
return TrackRenderer.STATE_PREPARED;
}
}
return TrackRenderer.STATE_IGNORE;
}
@Override
protected void onEnabled(long positionUs, boolean joining) {
source.enable(trackIndex, positionUs);
seekToInternal(positionUs);
}
@Override
protected void seekTo(long positionUs) throws ExoPlaybackException {
source.seekToUs(positionUs);
seekToInternal(positionUs);
}
private void seekToInternal(long positionUs) {
currentPositionUs = positionUs;
pendingMetadata = null;
inputStreamEnded = false;
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
currentPositionUs = positionUs;
try {
source.continueBuffering(positionUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
if (!inputStreamEnded && pendingMetadata == null) {
try {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
pendingMetadataTimestamp = sampleHolder.timeUs;
pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size);
sampleHolder.data.clear();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
if (pendingMetadata != null && pendingMetadataTimestamp <= currentPositionUs) {
invokeRenderer(pendingMetadata);
pendingMetadata = null;
}
}
@Override
protected void onDisabled() {
pendingMetadata = null;
source.disable(trackIndex);
}
@Override
protected long getDurationUs() {
return source.getTrackInfo(trackIndex).durationUs;
}
@Override
protected long getCurrentPositionUs() {
return currentPositionUs;
}
@Override
protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK_US;
}
@Override
protected boolean isEnded() {
return inputStreamEnded;
}
@Override
protected boolean isReady() {
return true;
}
private void invokeRenderer(List<Metadata> metadata) {
if (metadataHandler != null) {
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else {
invokeRendererInternal(metadata);
}
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVOKE_RENDERER:
invokeRendererInternal((List<Metadata>) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternal(List<Metadata> metadata) {
metadataRenderer.onMetadata(metadata);
}
}

View File

@ -33,6 +33,14 @@ public final class BitsArray {
private int byteOffset; private int byteOffset;
private int bitOffset; private int bitOffset;
public BitsArray() {
}
public BitsArray(byte[] data, int limit) {
this.data = data;
this.limit = limit;
}
/** /**
* Resets the state. * Resets the state.
*/ */
@ -240,6 +248,22 @@ public final class BitsArray {
return limit == 0; return limit == 0;
} }
/**
* Reads a Synchsafe integer.
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
* A 32 bit synchsafe integer can store 28 bits of information.
*
* @return The value of the parsed Synchsafe integer.
*/
public int readSynchSafeInt() {
int b1 = readUnsignedByte();
int b2 = readUnsignedByte();
int b3 = readUnsignedByte();
int b4 = readUnsignedByte();
return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
}
// TODO: Find a better place for this method. // TODO: Find a better place for this method.
/** /**
* Finds the next Adts sync word. * Finds the next Adts sync word.

View File

@ -58,6 +58,7 @@ public final class TsExtractor {
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_H264 = 0x1B; private static final int TS_STREAM_TYPE_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
@ -345,6 +346,9 @@ public final class TsExtractor {
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:
pesPayloadReader = new H264Reader(); pesPayloadReader = new H264Reader();
break; break;
case TS_STREAM_TYPE_ID3:
pesPayloadReader = new Id3Reader();
break;
} }
if (pesPayloadReader != null) { if (pesPayloadReader != null) {
@ -688,6 +692,23 @@ public final class TsExtractor {
} }
/**
* Parses ID3 data and extracts individual text information frames.
*/
private class Id3Reader extends PesPayloadReader {
public Id3Reader() {
setMediaFormat(MediaFormat.createId3Format());
}
@SuppressLint("InlinedApi")
@Override
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC);
}
}
/** /**
* Simplified version of SampleHolder for internal buffering. * Simplified version of SampleHolder for internal buffering.
*/ */

View File

@ -37,6 +37,7 @@ public class MimeTypes {
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
private MimeTypes() {} private MimeTypes() {}