Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple TXXX ID3 tags #1234

Merged
merged 6 commits into from
Feb 15, 2016
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
import com.google.android.exoplayer.demo.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.GeobMetadata;
import com.google.android.exoplayer.metadata.PrivMetadata;
import com.google.android.exoplayer.metadata.TxxxMetadata;
import com.google.android.exoplayer.metadata.frame.GeobFrame;
import com.google.android.exoplayer.metadata.frame.Id3Frame;
import com.google.android.exoplayer.metadata.frame.PrivFrame;
import com.google.android.exoplayer.metadata.frame.TxxxFrame;
import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.SubtitleLayout;
Expand Down Expand Up @@ -605,23 +606,23 @@ public void onCues(List<Cue> cues) {
// DemoPlayer.MetadataListener implementation

@Override
public void onId3Metadata(Map<String, Object> metadata) {
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
if (TxxxMetadata.TYPE.equals(entry.getKey())) {
TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
public void onId3Metadata(List<Id3Frame> id3Frames) {
for (Id3Frame id3Frame : id3Frames) {
if (id3Frame instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value));
} else if (PrivMetadata.TYPE.equals(entry.getKey())) {
PrivMetadata privMetadata = (PrivMetadata) entry.getValue();
id3Frame.getFrameId(), txxxFrame.getDescription(), txxxFrame.getValue()));
} else if (id3Frame instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s",
PrivMetadata.TYPE, privMetadata.owner));
} else if (GeobMetadata.TYPE.equals(entry.getKey())) {
GeobMetadata geobMetadata = (GeobMetadata) entry.getValue();
id3Frame.getFrameId(), privFrame.getOwner()));
} else if (id3Frame instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename,
geobMetadata.description));
id3Frame.getFrameId(), geobFrame.getMimeType(), geobFrame.getFilename(),
geobFrame.getDescription()));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey()));
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.getFrameId()));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer;
import com.google.android.exoplayer.metadata.frame.Id3Frame;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.upstream.BandwidthMeter;
Expand Down Expand Up @@ -60,7 +61,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {
MetadataRenderer<List<Id3Frame>>, DebugTextViewHelper.Provider {

/**
* Builds renderers for the player.
Expand Down Expand Up @@ -140,7 +141,7 @@ public interface CaptionListener {
* A listener for receiving ID3 metadata parsed from the media stream.
*/
public interface Id3MetadataListener {
void onId3Metadata(Map<String, Object> metadata);
void onId3Metadata(List<Id3Frame> id3Frames);
}

// Constants pulled into this class for convenience.
Expand Down Expand Up @@ -519,9 +520,9 @@ public void onCues(List<Cue> cues) {
}

@Override
public void onMetadata(Map<String, Object> metadata) {
public void onMetadata(List<Id3Frame> id3Frames) {
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
id3MetadataListener.onId3Metadata(metadata);
id3MetadataListener.onId3Metadata(id3Frames);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
import com.google.android.exoplayer.metadata.Id3Parser;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.metadata.frame.Id3Frame;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
Expand All @@ -47,6 +48,7 @@
import android.os.Handler;

import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
Expand Down Expand Up @@ -145,7 +147,7 @@ public void onSingleManifest(HlsPlaylist manifest) {
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
MetadataTrackRenderer<List<Id3Frame>> id3Renderer = new MetadataTrackRenderer<>(
sampleSource, new Id3Parser(), player, mainHandler.getLooper());

// Build the text renderer, preferring Webvtt where available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,49 @@
*/
package com.google.android.exoplayer.metadata;

import com.google.android.exoplayer.metadata.frame.Id3Frame;
import com.google.android.exoplayer.metadata.frame.TxxxFrame;

import junit.framework.TestCase;

import java.util.Map;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* Test for {@link Id3Parser}
*/
public class Id3ParserTest extends TestCase {

public void testParseTxxxFrames() {
byte[] rawId3 = new byte[] { 73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31,
0, 0, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50,
55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0 };

Id3Parser parser = new Id3Parser();
try {
Map<String, Object> metadata = parser.parse(rawId3, rawId3.length);
assertNotNull(metadata);
assertEquals(1, metadata.size());
TxxxMetadata txxx = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE);
assertNotNull(txxx);
assertEquals("", txxx.description);
assertEquals("mdialog_VINDICO1527664_start", txxx.value);
} catch (Exception exception) {
fail(exception.getMessage());
public void testParseTxxxFrames() {
byte[] rawId3 = new byte[]{ 73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31,
0, 0, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50,
55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0 };

Id3Parser parser = new Id3Parser();
try {
List<Id3Frame> id3Frames = parser.parse( rawId3, rawId3.length );
assertNotNull( id3Frames );
assertEquals( 1, id3Frames.size() );
List<TxxxFrame> txxxFrames = ofType( id3Frames, TxxxFrame.class );
assertEquals( 1, txxxFrames.size() );
TxxxFrame txxxFrame = txxxFrames.toArray( new TxxxFrame[1] )[0];
assertNotNull( txxxFrame );
assertEquals( "", txxxFrame.getDescription() );
assertEquals( "mdialog_VINDICO1527664_start", txxxFrame.getValue() );
} catch ( Exception exception ) {
fail( exception.getMessage() );
}
}

private static <T> List<T> ofType( Collection<? super T> col, Class<T> type ) {
final List<T> ret = new ArrayList<>();
for ( Object o : col ) {
if ( type.isInstance( o ) ) {
ret.add( ( T ) o );
}
}
return ret;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@
package com.google.android.exoplayer.metadata;

import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.metadata.frame.BinaryFrame;
import com.google.android.exoplayer.metadata.frame.GeobFrame;
import com.google.android.exoplayer.metadata.frame.Id3Frame;
import com.google.android.exoplayer.metadata.frame.PrivFrame;
import com.google.android.exoplayer.metadata.frame.TxxxFrame;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
* Extracts individual TXXX text frames from raw ID3 data.
*/
public final class Id3Parser implements MetadataParser<Map<String, Object>> {
public final class Id3Parser implements MetadataParser<List<Id3Frame>> {

private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
Expand All @@ -41,9 +46,9 @@ public boolean canParse(String mimeType) {
}

@Override
public Map<String, Object> parse(byte[] data, int size)
throws UnsupportedEncodingException, ParserException {
Map<String, Object> metadata = new HashMap<>();
public List<Id3Frame> parse( byte[] data, int size)
throws UnsupportedEncodingException, ParserException {
List<Id3Frame> id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size);
int id3Size = parseId3Header(id3Data);

Expand Down Expand Up @@ -71,8 +76,8 @@ public Map<String, Object> parse(byte[] data, int size)
int valueStartIndex = firstZeroIndex + delimiterLength(encoding);
int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex,
charset);
metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value));
charset);
id3Frames.add(new TxxxFrame(description, value));
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
// Check frame ID == PRIV
byte[] frame = new byte[frameSize];
Expand All @@ -82,7 +87,7 @@ public Map<String, Object> parse(byte[] data, int size)
String owner = new String(frame, 0, firstZeroIndex, "ISO-8859-1");
byte[] privateData = new byte[frameSize - firstZeroIndex - 1];
System.arraycopy(frame, firstZeroIndex + 1, privateData, 0, frameSize - firstZeroIndex - 1);
metadata.put(PrivMetadata.TYPE, new PrivMetadata(owner, privateData));
id3Frames.add(new PrivFrame(owner, privateData));
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
// Check frame ID == GEOB
int encoding = id3Data.readUnsignedByte();
Expand All @@ -95,30 +100,29 @@ public Map<String, Object> parse(byte[] data, int size)
int filenameStartIndex = firstZeroIndex + 1;
int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding);
String filename = new String(frame, filenameStartIndex,
filenameEndIndex - filenameStartIndex, charset);
filenameEndIndex - filenameStartIndex, charset);
int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding);
String description = new String(frame, descriptionStartIndex,
descriptionEndIndex - descriptionStartIndex, charset);
descriptionEndIndex - descriptionStartIndex, charset);

int objectDataSize = frameSize - 1 /* encoding byte */ - descriptionEndIndex
- delimiterLength(encoding);
- delimiterLength(encoding);
byte[] objectData = new byte[objectDataSize];
System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0,
objectDataSize);
metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename,
description, objectData));
objectDataSize);
id3Frames.add(new GeobFrame(mimeType, filename, description, objectData));
} else {
String type = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize];
id3Data.readBytes(frame, 0, frameSize);
metadata.put(type, frame);
id3Frames.add(new BinaryFrame(type,frame));
}

id3Size -= frameSize + 10 /* header size */;
}

return Collections.unmodifiableMap(metadata);
return Collections.unmodifiableList(id3Frames);
}

private static int indexOf(byte[] data, int fromIndex, byte key) {
Expand Down Expand Up @@ -151,7 +155,7 @@ private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) {

private static int delimiterLength(int encodingByte) {
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1
|| encodingByte == ID3_TEXT_ENCODING_UTF_8) ? 1 : 2;
|| encodingByte == ID3_TEXT_ENCODING_UTF_8) ? 1 : 2;
}

/**
Expand All @@ -167,7 +171,7 @@ private static int parseId3Header(ParsableByteArray id3Buffer) throws ParserExce
int id3 = id3Buffer.readUnsignedByte();
if (id1 != 'I' || id2 != 'D' || id3 != '3') {
throw new ParserException(String.format(Locale.US,
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3));
}
id3Buffer.skipBytes(2); // Skip version.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.google.android.exoplayer.metadata.frame;

/**
* Binary ID3 frame
*/
public class BinaryFrame extends Id3Frame {

private final byte[] data;

public BinaryFrame( String type, byte[] data ) {
super(type);
this.data = data;
}

public byte[] getBytes() {
return data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
package com.google.android.exoplayer.metadata.frame;

/**
* A metadata that contains parsed ID3 GEOB (General Encapsulated Object) frame data associated
* with time indices.
*/
public final class GeobMetadata {
public final class GeobFrame extends Id3Frame {

public static final String TYPE = "GEOB";
public static final String ID = "GEOB";

public final String mimeType;
public final String filename;
public final String description;
public final byte[] data;
private final String mimeType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have you made all of these private? We have a preference for making fields public if they're final, rather than having getter methods. Ditto in other files (including the new BinaryFrame file).

private final String filename;
private final String description;
private final byte[] data;

public GeobMetadata(String mimeType, String filename, String description, byte[] data) {
public GeobFrame( String mimeType, String filename, String description, byte[] data) {
super(ID);
this.mimeType = mimeType;
this.filename = filename;
this.description = description;
this.data = data;
}

public String getMimeType() {
return mimeType;
}

public String getFilename() {
return filename;
}

public String getDescription() {
return description;
}

public byte[] getData() {
return data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.google.android.exoplayer.metadata.frame;

/**
* Base abstract class for ID3 frames
*/
public abstract class Id3Frame {

private final String frameId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make public also.


public Id3Frame( String frameId ){
this.frameId = frameId;
}

public String getFrameId() {
return frameId;
}
}
Loading