From 15854ec5f9cc86607b48ba3dfaa79434055fbc75 Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Fri, 22 Jul 2022 23:54:03 +1000 Subject: [PATCH] Implement MappingAudioSource on Android Inspiration taken from: - https://github.com/google/ExoPlayer/issues/7087 - https://github.com/google/ExoPlayer/issues/5883 - https://github.com/google/ExoPlayer/issues/7279 --- .../com/ryanheise/just_audio/AudioPlayer.java | 37 +++- .../ryanheise/just_audio/LazyMediaSource.java | 160 ++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 just_audio/android/src/main/java/com/ryanheise/just_audio/LazyMediaSource.java diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java index d31c284d..45032a1e 100644 --- a/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.metadata.icy.IcyInfo; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; @@ -62,7 +63,7 @@ import java.util.Map; import java.util.Random; -public class AudioPlayer implements MethodCallHandler, Player.Listener, MetadataOutput { +public class AudioPlayer implements MethodCallHandler, Player.Listener, MetadataOutput, LazyMediaSourceProvider { static final String TAG = "AudioPlayer"; @@ -611,6 +612,17 @@ private MediaSource decodeAudioSource(final Object json) { .setUri(Uri.parse((String)map.get("uri"))) .setMimeType(MimeTypes.APPLICATION_M3U8) .build()); + case "mapping": + return new MaskingMediaSource( + new LazyMediaSource( + this, + id, + new MediaItem.Builder() + .setTag(id) + .build() + ), + true + ); case "silence": return new SilenceMediaSource.Factory() .setDurationUs(getLong(map.get("duration"))) @@ -695,6 +707,29 @@ private DataSource.Factory buildDataSourceFactory() { return new DefaultDataSource.Factory(context, httpDataSourceFactory); } + @Override + public void createMediaSource(String id, LazyMediaSourceReceiver receiver) { + handler.post(() -> { + methodChannel.invokeMethod("createMappedAudioSourceSource", Collections.singletonMap("id", id), new Result() { + @Override + public void success(Object json) { + final MediaSource mediaSource = decodeAudioSource(json); + receiver.onMediaSourceCreated(mediaSource); + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) { + throw new IllegalStateException("createMappedAudioSourceSource failed. Cannot proceed. (" + errorCode + ", " + errorMessage + ", " + errorDetails + ")"); + } + + @Override + public void notImplemented() { + throw new IllegalArgumentException("createMappedAudioSourceSource is not implemented by the platform."); + } + }); + }); + } + private void load(final MediaSource mediaSource, final long initialPosition, final Integer initialIndex, final Result result) { this.initialPos = initialPosition; this.initialIndex = initialIndex; diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/LazyMediaSource.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/LazyMediaSource.java new file mode 100644 index 00000000..56b82f66 --- /dev/null +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/LazyMediaSource.java @@ -0,0 +1,160 @@ +package com.ryanheise.just_audio; + +import android.os.Handler; + +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.PlayerId; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MediaSource} that lazily defers to another {@link MediaSource} when it is required. + *

+ * This {@link MediaSource} must be used with a {@link com.google.android.exoplayer2.source.MaskingMediaSource}. + */ +class LazyMediaSource implements MediaSource { + private final LazyMediaSourceProvider mediaSourceProvider; + public final String id; + public final MediaItem placeholderMediaItem; + + private final Map pendingEventListeners = new HashMap<>(); + private final Map pendingDrmEventListeners = new HashMap<>(); + + private MediaSource mediaSource; + + LazyMediaSource(LazyMediaSourceProvider mediaSourceProvider, String id, MediaItem placeholderMediaItem) { + this.mediaSourceProvider = mediaSourceProvider; + this.id = id; + this.placeholderMediaItem = placeholderMediaItem; + } + + @Override + public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + if (mediaSource == null) { + pendingEventListeners.put(eventListener, handler); + } else { + mediaSource.addEventListener(handler, eventListener); + } + } + + @Override + public void removeEventListener(MediaSourceEventListener eventListener) { + if (mediaSource == null) { + pendingEventListeners.remove(eventListener); + } else { + mediaSource.removeEventListener(eventListener); + } + } + + @Override + public void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) { + if (mediaSource == null) { + pendingDrmEventListeners.put(eventListener, handler); + } else { + mediaSource.addDrmEventListener(handler, eventListener); + } + } + + @Override + public void removeDrmEventListener(DrmSessionEventListener eventListener) { + if (mediaSource == null) { + pendingDrmEventListeners.remove(eventListener); + } else { + mediaSource.removeDrmEventListener(eventListener); + } + } + + @Override + public Timeline getInitialTimeline() { + if (mediaSource == null) return null; + return mediaSource.getInitialTimeline(); + } + + @Override + public boolean isSingleWindow() { + if (mediaSource == null) return false; + return mediaSource.isSingleWindow(); + } + + @Override + public MediaItem getMediaItem() { + if (mediaSource == null) { + return placeholderMediaItem; + } else { + return mediaSource.getMediaItem(); + } + } + + @Override + public void prepareSource( + MediaSourceCaller caller, + TransferListener mediaTransferListener, + PlayerId playerId + ) { + mediaSourceProvider.createMediaSource(id, (mediaSource) -> { + this.mediaSource = mediaSource; + for (Map.Entry entry : pendingEventListeners.entrySet()) { + mediaSource.addEventListener(entry.getValue(), entry.getKey()); + } + pendingEventListeners.clear(); + for (Map.Entry entry : pendingDrmEventListeners.entrySet()) { + mediaSource.addDrmEventListener(entry.getValue(), entry.getKey()); + } + pendingDrmEventListeners.clear(); + mediaSource.prepareSource(caller, mediaTransferListener, playerId); + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + if (mediaSource == null) return; + mediaSource.maybeThrowSourceInfoRefreshError(); + } + + @Override + public void enable(MediaSourceCaller caller) { + if (mediaSource == null) throw new IllegalStateException(); + mediaSource.enable(caller); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + if (mediaSource == null) throw new IllegalStateException(); + return mediaSource.createPeriod(id, allocator, startPositionUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (mediaSource == null) throw new IllegalStateException(); + mediaSource.releasePeriod(mediaPeriod); + } + + @Override + public void disable(MediaSourceCaller caller) { + if (mediaSource == null) throw new IllegalStateException(); + mediaSource.disable(caller); + } + + @Override + public void releaseSource(MediaSourceCaller caller) { + if (mediaSource == null) return; + mediaSource.releaseSource(caller); + } +} + +interface LazyMediaSourceReceiver { + void onMediaSourceCreated(MediaSource mediaSource); +} + +interface LazyMediaSourceProvider { + void createMediaSource(String id, LazyMediaSourceReceiver receiver); +} \ No newline at end of file