mirror of
https://github.com/samsonjs/media.git
synced 2026-04-05 11:15:46 +00:00
1. Rename "extensions" package to "decoder". It's used by both text and (should be used by) metadata, so it's no longer just for extensions. 2. Move Buffer objects move into the decoder package. 3. Rename SubtitleParser and MetadataParser to SubtitleDecoder and MetadataDecoder respectively, since they extend Decoder. Ditto for all subclasses. 4. Subtitle and Metadata decoders now throw their own exception types rather than ParserException. 5. Move MediaCodec classes into a mediacodec package, with the exception of the concrete audio and video renderers. 6. Create an audio package to hold the two audio renderer classes plus related util classes. 7. Create a video package to hold the one video renderer class plus related util classes. After this change the following nice properties hold: 1. Want a video renderer? Look in the video package. Ditto for audio, text and metadata. 2. All TrackRenderer implementations use a decoder of some kind to decode buffers received from the source, so we have consistent terminology there. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=127326805
619 lines
19 KiB
Java
619 lines
19 KiB
Java
/*
|
|
* Copyright (C) 2016 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.exoplayer2;
|
|
|
|
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
|
import com.google.android.exoplayer2.audio.AudioTrack;
|
|
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
|
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
|
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
|
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
|
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
|
import com.google.android.exoplayer2.source.MediaSource;
|
|
import com.google.android.exoplayer2.text.Cue;
|
|
import com.google.android.exoplayer2.text.TextRenderer;
|
|
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
|
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
|
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.media.AudioManager;
|
|
import android.media.MediaCodec;
|
|
import android.media.PlaybackParams;
|
|
import android.os.Handler;
|
|
import android.util.Log;
|
|
import android.view.Surface;
|
|
|
|
import java.lang.reflect.Constructor;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* An {@link ExoPlayer} that uses default {@link Renderer} components.
|
|
* <p>
|
|
* Instances of this class can be obtained from {@link ExoPlayerFactory}.
|
|
*/
|
|
@TargetApi(16)
|
|
public final class SimpleExoPlayer implements ExoPlayer {
|
|
|
|
/**
|
|
* A listener for video rendering information.
|
|
*/
|
|
public interface VideoListener {
|
|
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
|
float pixelWidthHeightRatio);
|
|
void onDrawnToSurface(Surface surface);
|
|
}
|
|
|
|
/**
|
|
* A listener for debugging information.
|
|
*/
|
|
public interface DebugListener {
|
|
void onAudioEnabled(DecoderCounters counters);
|
|
void onAudioSessionId(int audioSessionId);
|
|
void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
|
long initializationDurationMs);
|
|
void onAudioFormatChanged(Format format);
|
|
void onAudioDisabled(DecoderCounters counters);
|
|
void onVideoEnabled(DecoderCounters counters);
|
|
void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
|
long initializationDurationMs);
|
|
void onVideoFormatChanged(Format format);
|
|
void onVideoDisabled(DecoderCounters counters);
|
|
void onDroppedFrames(int count, long elapsed);
|
|
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
|
}
|
|
|
|
/**
|
|
* A listener for receiving notifications of timed text.
|
|
*/
|
|
public interface CaptionListener {
|
|
void onCues(List<Cue> cues);
|
|
}
|
|
|
|
/**
|
|
* A listener for receiving ID3 metadata parsed from the media stream.
|
|
*/
|
|
public interface Id3MetadataListener {
|
|
void onId3Metadata(List<Id3Frame> id3Frames);
|
|
}
|
|
|
|
private static final String TAG = "SimpleExoPlayer";
|
|
private static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
|
|
|
|
private final ExoPlayer player;
|
|
private final Renderer[] renderers;
|
|
private final ComponentListener componentListener;
|
|
private final Handler mainHandler;
|
|
private final int videoRendererCount;
|
|
private final int audioRendererCount;
|
|
|
|
private Format videoFormat;
|
|
private Format audioFormat;
|
|
|
|
private CaptionListener captionListener;
|
|
private Id3MetadataListener id3MetadataListener;
|
|
private VideoListener videoListener;
|
|
private DebugListener debugListener;
|
|
private DecoderCounters videoDecoderCounters;
|
|
private DecoderCounters audioDecoderCounters;
|
|
private int audioSessionId;
|
|
|
|
/* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector,
|
|
LoadControl loadControl, DrmSessionManager drmSessionManager,
|
|
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) {
|
|
mainHandler = new Handler();
|
|
componentListener = new ComponentListener();
|
|
|
|
// Build the renderers.
|
|
ArrayList<Renderer> renderersList = new ArrayList<>();
|
|
if (preferExtensionDecoders) {
|
|
buildExtensionRenderers(renderersList, allowedVideoJoiningTimeMs);
|
|
buildRenderers(context, drmSessionManager, renderersList, allowedVideoJoiningTimeMs);
|
|
} else {
|
|
buildRenderers(context, drmSessionManager, renderersList, allowedVideoJoiningTimeMs);
|
|
buildExtensionRenderers(renderersList, allowedVideoJoiningTimeMs);
|
|
}
|
|
renderers = renderersList.toArray(new Renderer[renderersList.size()]);
|
|
|
|
// Obtain counts of video and audio renderers.
|
|
int videoRendererCount = 0;
|
|
int audioRendererCount = 0;
|
|
for (Renderer renderer : renderers) {
|
|
switch (renderer.getTrackType()) {
|
|
case C.TRACK_TYPE_VIDEO:
|
|
videoRendererCount++;
|
|
break;
|
|
case C.TRACK_TYPE_AUDIO:
|
|
audioRendererCount++;
|
|
break;
|
|
}
|
|
}
|
|
this.videoRendererCount = videoRendererCount;
|
|
this.audioRendererCount = audioRendererCount;
|
|
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
|
|
|
// Build the player and associated objects.
|
|
player = new ExoPlayerImpl(renderers, trackSelector, loadControl);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of renderers.
|
|
*
|
|
* @return The number of renderers.
|
|
*/
|
|
public int getRendererCount() {
|
|
return renderers.length;
|
|
}
|
|
|
|
/**
|
|
* Returns the track type that the renderer at a given index handles.
|
|
*
|
|
* @see Renderer#getTrackType()
|
|
* @param index The index of the renderer.
|
|
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
|
|
*/
|
|
public int getRendererType(int index) {
|
|
return renderers[index].getTrackType();
|
|
}
|
|
|
|
/**
|
|
* Sets the {@link Surface} onto which video will be rendered.
|
|
*
|
|
* @param surface The {@link Surface}.
|
|
*/
|
|
public void setSurface(Surface surface) {
|
|
ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
|
|
int count = 0;
|
|
for (Renderer renderer : renderers) {
|
|
if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
|
|
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
|
|
}
|
|
}
|
|
if (surface == null) {
|
|
// Block to ensure that the surface is not accessed after the method returns.
|
|
player.blockingSendMessages(messages);
|
|
} else {
|
|
player.sendMessages(messages);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the audio volume, with 0 being silence and 1 being unity gain.
|
|
*
|
|
* @param volume The volume.
|
|
*/
|
|
public void setVolume(float volume) {
|
|
ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
|
|
int count = 0;
|
|
for (Renderer renderer : renderers) {
|
|
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
|
|
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, volume);
|
|
}
|
|
}
|
|
player.sendMessages(messages);
|
|
}
|
|
|
|
/**
|
|
* Sets {@link PlaybackParams} governing audio playback.
|
|
*
|
|
* @param params The {@link PlaybackParams}.
|
|
*/
|
|
public void setPlaybackParams(PlaybackParams params) {
|
|
ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
|
|
int count = 0;
|
|
for (Renderer renderer : renderers) {
|
|
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
|
|
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_PLAYBACK_PARAMS, params);
|
|
}
|
|
}
|
|
player.sendMessages(messages);
|
|
}
|
|
|
|
/**
|
|
* @return The video format currently being played, or null if there is no video component to the
|
|
* current media.
|
|
*/
|
|
public Format getVideoFormat() {
|
|
return videoFormat;
|
|
}
|
|
|
|
/**
|
|
* @return The audio format currently being played, or null if there is no audio component to the
|
|
* current media.
|
|
*/
|
|
public Format getAudioFormat() {
|
|
return audioFormat;
|
|
}
|
|
|
|
/**
|
|
* @return The audio session identifier. If not set {@code AudioTrack.SESSION_ID_NOT_SET} is
|
|
* returned.
|
|
*/
|
|
public int getAudioSessionId() {
|
|
return audioSessionId;
|
|
}
|
|
|
|
/**
|
|
* @return The {@link DecoderCounters} for video, or null if there is no video component to the
|
|
* current media.
|
|
*/
|
|
public DecoderCounters getVideoDecoderCounters() {
|
|
return videoDecoderCounters;
|
|
}
|
|
|
|
/**
|
|
* @return The {@link DecoderCounters} for audio, or null if there is no audio component to the
|
|
* current media.
|
|
*/
|
|
public DecoderCounters getAudioDecoderCounters() {
|
|
return audioDecoderCounters;
|
|
}
|
|
|
|
/**
|
|
* Sets a listener to receive video events.
|
|
*
|
|
* @param listener The listener.
|
|
*/
|
|
public void setVideoListener(VideoListener listener) {
|
|
videoListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Sets a listener to receive debug events.
|
|
*
|
|
* @param listener The listener.
|
|
*/
|
|
public void setDebugListener(DebugListener listener) {
|
|
debugListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Sets a listener to receive caption events.
|
|
*
|
|
* @param listener The listener.
|
|
*/
|
|
public void setCaptionListener(CaptionListener listener) {
|
|
captionListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Sets a listener to receive metadata events.
|
|
*
|
|
* @param listener The listener.
|
|
*/
|
|
public void setMetadataListener(Id3MetadataListener listener) {
|
|
id3MetadataListener = listener;
|
|
}
|
|
|
|
// ExoPlayer implementation
|
|
|
|
@Override
|
|
public void addListener(EventListener listener) {
|
|
player.addListener(listener);
|
|
}
|
|
|
|
@Override
|
|
public void removeListener(EventListener listener) {
|
|
player.removeListener(listener);
|
|
}
|
|
|
|
@Override
|
|
public int getPlaybackState() {
|
|
return player.getPlaybackState();
|
|
}
|
|
|
|
@Override
|
|
public void setMediaSource(MediaSource mediaSource) {
|
|
player.setMediaSource(mediaSource);
|
|
}
|
|
|
|
@Override
|
|
public void setPlayWhenReady(boolean playWhenReady) {
|
|
player.setPlayWhenReady(playWhenReady);
|
|
}
|
|
|
|
@Override
|
|
public boolean getPlayWhenReady() {
|
|
return player.getPlayWhenReady();
|
|
}
|
|
|
|
@Override
|
|
public boolean isPlayWhenReadyCommitted() {
|
|
return player.isPlayWhenReadyCommitted();
|
|
}
|
|
|
|
@Override
|
|
public boolean isLoading() {
|
|
return player.isLoading();
|
|
}
|
|
|
|
@Override
|
|
public void seekTo(long positionMs) {
|
|
player.seekTo(positionMs);
|
|
}
|
|
|
|
@Override
|
|
public void seekTo(int periodIndex, long positionMs) {
|
|
player.seekTo(periodIndex, positionMs);
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
player.stop();
|
|
}
|
|
|
|
@Override
|
|
public void release() {
|
|
player.release();
|
|
}
|
|
|
|
@Override
|
|
public void sendMessages(ExoPlayerMessage... messages) {
|
|
player.sendMessages(messages);
|
|
}
|
|
|
|
@Override
|
|
public void blockingSendMessages(ExoPlayerMessage... messages) {
|
|
player.blockingSendMessages(messages);
|
|
}
|
|
|
|
@Override
|
|
public long getDuration() {
|
|
return player.getDuration();
|
|
}
|
|
|
|
@Override
|
|
public long getCurrentPosition() {
|
|
return player.getCurrentPosition();
|
|
}
|
|
|
|
@Override
|
|
public int getCurrentPeriodIndex() {
|
|
return player.getCurrentPeriodIndex();
|
|
}
|
|
|
|
@Override
|
|
public long getBufferedPosition() {
|
|
return player.getBufferedPosition();
|
|
}
|
|
|
|
@Override
|
|
public int getBufferedPercentage() {
|
|
return player.getBufferedPercentage();
|
|
}
|
|
|
|
// Internal methods.
|
|
|
|
private void buildRenderers(Context context, DrmSessionManager drmSessionManager,
|
|
ArrayList<Renderer> renderersList, long allowedVideoJoiningTimeMs) {
|
|
MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer(context,
|
|
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
|
allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, componentListener,
|
|
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
|
|
renderersList.add(videoRenderer);
|
|
|
|
Renderer audioRenderer = new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT,
|
|
drmSessionManager, true, mainHandler, componentListener,
|
|
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
|
renderersList.add(audioRenderer);
|
|
|
|
Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper());
|
|
renderersList.add(textRenderer);
|
|
|
|
MetadataRenderer<List<Id3Frame>> id3Renderer = new MetadataRenderer<>(componentListener,
|
|
mainHandler.getLooper(), new Id3Decoder());
|
|
renderersList.add(id3Renderer);
|
|
}
|
|
|
|
private void buildExtensionRenderers(ArrayList<Renderer> renderersList,
|
|
long allowedVideoJoiningTimeMs) {
|
|
// Load extension renderers using reflection so that demo app doesn't depend on them.
|
|
// Class.forName(<class name>) appears for each renderer so that automated tools like proguard
|
|
// can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
|
|
try {
|
|
Class<?> clazz =
|
|
Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
|
|
Constructor<?> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class,
|
|
VideoRendererEventListener.class, int.class);
|
|
renderersList.add((Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
|
|
mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
|
|
Log.i(TAG, "Loaded LibvpxVideoRenderer.");
|
|
} catch (ClassNotFoundException e) {
|
|
// Expected if the app was built without the extension.
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
try {
|
|
Class<?> clazz =
|
|
Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
|
|
Constructor<?> constructor = clazz.getConstructor(Handler.class,
|
|
AudioRendererEventListener.class);
|
|
renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener));
|
|
Log.i(TAG, "Loaded LibopusAudioRenderer.");
|
|
} catch (ClassNotFoundException e) {
|
|
// Expected if the app was built without the extension.
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
try {
|
|
Class<?> clazz =
|
|
Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
|
|
Constructor<?> constructor = clazz.getConstructor(Handler.class,
|
|
AudioRendererEventListener.class);
|
|
renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener));
|
|
Log.i(TAG, "Loaded LibflacAudioRenderer.");
|
|
} catch (ClassNotFoundException e) {
|
|
// Expected if the app was built without the extension.
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
try {
|
|
Class<?> clazz =
|
|
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
|
|
Constructor<?> constructor = clazz.getConstructor(Handler.class,
|
|
AudioRendererEventListener.class);
|
|
renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener));
|
|
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
|
|
} catch (ClassNotFoundException e) {
|
|
// Expected if the app was built without the extension.
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private final class ComponentListener implements VideoRendererEventListener,
|
|
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<List<Id3Frame>> {
|
|
|
|
// VideoRendererEventListener implementation
|
|
|
|
@Override
|
|
public void onVideoEnabled(DecoderCounters counters) {
|
|
videoDecoderCounters = counters;
|
|
if (debugListener != null) {
|
|
debugListener.onVideoEnabled(counters);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
|
|
long initializationDurationMs) {
|
|
if (debugListener != null) {
|
|
debugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
|
|
initializationDurationMs);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onVideoInputFormatChanged(Format format) {
|
|
videoFormat = format;
|
|
if (debugListener != null) {
|
|
debugListener.onVideoFormatChanged(format);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDroppedFrames(int count, long elapsed) {
|
|
if (debugListener != null) {
|
|
debugListener.onDroppedFrames(count, elapsed);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
|
float pixelWidthHeightRatio) {
|
|
if (videoListener != null) {
|
|
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
|
|
pixelWidthHeightRatio);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDrawnToSurface(Surface surface) {
|
|
if (videoListener != null) {
|
|
videoListener.onDrawnToSurface(surface);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onVideoDisabled(DecoderCounters counters) {
|
|
if (debugListener != null) {
|
|
debugListener.onVideoDisabled(counters);
|
|
}
|
|
videoFormat = null;
|
|
videoDecoderCounters = null;
|
|
}
|
|
|
|
// AudioRendererEventListener implementation
|
|
|
|
@Override
|
|
public void onAudioEnabled(DecoderCounters counters) {
|
|
audioDecoderCounters = counters;
|
|
if (debugListener != null) {
|
|
debugListener.onAudioEnabled(counters);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAudioSessionId(int sessionId) {
|
|
audioSessionId = sessionId;
|
|
if (debugListener != null) {
|
|
debugListener.onAudioSessionId(sessionId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
|
|
long initializationDurationMs) {
|
|
if (debugListener != null) {
|
|
debugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
|
|
initializationDurationMs);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAudioInputFormatChanged(Format format) {
|
|
audioFormat = format;
|
|
if (debugListener != null) {
|
|
debugListener.onAudioFormatChanged(format);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
|
long elapsedSinceLastFeedMs) {
|
|
if (debugListener != null) {
|
|
debugListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAudioDisabled(DecoderCounters counters) {
|
|
if (debugListener != null) {
|
|
debugListener.onAudioDisabled(counters);
|
|
}
|
|
audioFormat = null;
|
|
audioDecoderCounters = null;
|
|
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
|
}
|
|
|
|
// TextRendererOutput implementation
|
|
|
|
@Override
|
|
public void onCues(List<Cue> cues) {
|
|
if (captionListener != null) {
|
|
captionListener.onCues(cues);
|
|
}
|
|
}
|
|
|
|
// MetadataRenderer implementation
|
|
|
|
@Override
|
|
public void onMetadata(List<Id3Frame> id3Frames) {
|
|
if (id3MetadataListener != null) {
|
|
id3MetadataListener.onId3Metadata(id3Frames);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|