Add ID3 Timed Metadata support for HLS #67
This commit is contained in:
parent
ca31010028
commit
d3a05c9a44
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
@ -689,8 +693,25 @@ public final class TsExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplified version of SampleHolder for internal buffering.
|
* 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.
|
||||||
|
*/
|
||||||
private static class Sample {
|
private static class Sample {
|
||||||
|
|
||||||
public byte[] data;
|
public byte[] data;
|
||||||
|
@ -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() {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user