1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.photopicker.ui.remotepreview;
18 
19 import static android.provider.CloudMediaProvider.CloudMediaSurfaceController;
20 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback;
21 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING;
22 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED;
23 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED;
24 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED;
25 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY;
26 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED;
27 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
28 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
29 
30 import android.annotation.DurationMillisLong;
31 import android.content.ContentResolver;
32 import android.content.Context;
33 import android.graphics.Point;
34 import android.media.AudioManager;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.util.Log;
40 import android.view.Surface;
41 
42 import androidx.annotation.NonNull;
43 
44 import com.android.providers.media.PickerUriResolver;
45 
46 import com.google.android.exoplayer2.DefaultLoadControl;
47 import com.google.android.exoplayer2.DefaultRenderersFactory;
48 import com.google.android.exoplayer2.ExoPlayer;
49 import com.google.android.exoplayer2.LoadControl;
50 import com.google.android.exoplayer2.MediaItem;
51 import com.google.android.exoplayer2.Player;
52 import com.google.android.exoplayer2.Player.State;
53 import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector;
54 import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
55 import com.google.android.exoplayer2.source.ProgressiveMediaSource;
56 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
57 import com.google.android.exoplayer2.upstream.ContentDataSource;
58 import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
59 import com.google.android.exoplayer2.util.Clock;
60 import com.google.android.exoplayer2.video.VideoSize;
61 
62 /**
63  * Implements a {@link CloudMediaSurfaceController} for a cloud provider authority and initializes
64  * an ExoPlayer instance to render cloud media to {@link Surface} instances.
65  */
66 public class RemoteSurfaceController extends CloudMediaSurfaceController {
67     private static final String TAG = "RemoteSurfaceController";
68 
69     // The minimum duration of media that the player will attempt to ensure is buffered at all
70     // times.
71     private static final int MIN_BUFFER_MS = 1000;
72     // The maximum duration of media that the player will attempt to buffer.
73     private static final int MAX_BUFFER_MS = 2000;
74     // The duration of media that must be buffered for playback to start or resume following a
75     // user action such as a seek.
76     private static final int BUFFER_FOR_PLAYBACK_MS = 1000;
77     // The default duration of media that must be buffered for playback to resume after a
78     // rebuffer.
79     private static final int BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 1000;
80     private static final LoadControl sLoadControl = new DefaultLoadControl.Builder()
81             .setBufferDurationsMs(
82                     MIN_BUFFER_MS,
83                     MAX_BUFFER_MS,
84                     BUFFER_FOR_PLAYBACK_MS,
85                     BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
86 
87     private final String mAuthority;
88     private final Context mContext;
89     private final CloudMediaSurfaceStateChangedCallback mCallback;
90     private final Handler mHandler = new Handler(Looper.getMainLooper());
91     private final Player.Listener mEventListener = new Player.Listener() {
92             @Override
93             public void onPlaybackStateChanged(@State int state) {
94                 Log.d(TAG, "Received player event " + state);
95 
96                 switch (state) {
97                     case Player.STATE_READY:
98                         mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_READY,
99                                 null);
100                         return;
101                     case Player.STATE_BUFFERING:
102                         mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_BUFFERING,
103                                 null);
104                         return;
105                     case Player.STATE_ENDED:
106                         mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_COMPLETED,
107                                 null);
108                         return;
109                     default:
110                 }
111             }
112 
113             @Override
114             public void onIsPlayingChanged(boolean isPlaying) {
115                 mCallback.setPlaybackState(mCurrentSurfaceId, isPlaying ? PLAYBACK_STATE_STARTED :
116                         PLAYBACK_STATE_PAUSED, null);
117             }
118 
119             @Override
120             public void onVideoSizeChanged(VideoSize videoSize) {
121                 Point size = new Point(videoSize.width, videoSize.height);
122                 Bundle bundle = new Bundle();
123                 bundle.putParcelable(ContentResolver.EXTRA_SIZE, size);
124                 mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
125                         bundle);
126             }
127         };
128 
129     private boolean mEnableLoop;
130     private boolean mMuteAudio;
131     private ExoPlayer mPlayer;
132     private int mCurrentSurfaceId = -1;
133 
RemoteSurfaceController(Context context, String authority, boolean enableLoop, boolean muteAudio, CloudMediaSurfaceStateChangedCallback callback)134     public RemoteSurfaceController(Context context, String authority, boolean enableLoop,
135             boolean muteAudio, CloudMediaSurfaceStateChangedCallback callback) {
136         mAuthority = authority;
137         mCallback = callback;
138         mContext = context;
139         mEnableLoop = enableLoop;
140         mMuteAudio = muteAudio;
141         Log.d(TAG, "Surface controller created.");
142     }
143 
144     @Override
onPlayerCreate()145     public void onPlayerCreate() {
146         mHandler.post(() -> {
147             mPlayer = createExoPlayer();
148             mPlayer.addListener(mEventListener);
149             updateLoopingPlaybackStatus();
150             updateAudioMuteStatus();
151             Log.d(TAG, "Player created.");
152         });
153     }
154 
155     @Override
onPlayerRelease()156     public void onPlayerRelease() {
157         mHandler.post(() -> {
158             mPlayer.removeListener(mEventListener);
159             mPlayer.release();
160             mPlayer = null;
161             Log.d(TAG, "Player released.");
162         });
163     }
164 
165     @Override
onSurfaceCreated(int surfaceId, @NonNull Surface surface, @NonNull String mediaId)166     public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
167             @NonNull String mediaId) {
168         mHandler.post(() -> {
169             try {
170                 // onSurfaceCreated may get called while the player is already rendering on a
171                 // different surface. In that case, pause the player before preparing it for
172                 // rendering on the new surface.
173                 // Unfortunately, Exoplayer#stop doesn't seem to work here. If we call stop(),
174                 // as soon as the player becomes ready again, it automatically starts to play
175                 // the new media. The reason is that Exoplayer treats play/pause as calls to
176                 // the method Exoplayer#setPlayWhenReady(boolean) with true and false
177                 // respectively. So, if we don't pause(), then since the previous play() call
178                 // had set setPlayWhenReady to true, the player would start the playback as soon
179                 // as it gets ready with the new media item.
180                 if (mPlayer.isPlaying()) {
181                     mPlayer.pause();
182                 }
183 
184                 mCurrentSurfaceId = surfaceId;
185 
186                 final Uri mediaUri = PickerUriResolver.getMediaUri(mAuthority).buildUpon()
187                         .appendPath(mediaId).build();
188                 mPlayer.setMediaItem(MediaItem.fromUri(mediaUri));
189                 mPlayer.setVideoSurface(surface);
190                 mPlayer.prepare();
191 
192                 Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
193                         + ". MediaId: " + mediaId);
194             } catch (RuntimeException e) {
195                 Log.e(TAG, "Error preparing player with surface.", e);
196             }
197         });
198     }
199 
200     @Override
onSurfaceChanged(int surfaceId, int format, int width, int height)201     public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
202         Log.d(TAG, "Surface changed: " + surfaceId + ". Format: " + format + ". Width: "
203                 + width + ". Height: " + height);
204     }
205 
206     @Override
onSurfaceDestroyed(int surfaceId)207     public void onSurfaceDestroyed(int surfaceId) {
208         mHandler.post(() -> {
209             if (mCurrentSurfaceId != surfaceId) {
210                 // This means that the player is already using some other surface, hence
211                 // nothing to do.
212                 return;
213             }
214             if (mPlayer.isPlaying()) {
215                 mPlayer.stop();
216             }
217             mPlayer.clearVideoSurface();
218             mCurrentSurfaceId = -1;
219 
220             Log.d(TAG, "Surface released: " + surfaceId);
221         });
222     }
223 
224     @Override
onMediaPlay(int surfaceId)225     public void onMediaPlay(int surfaceId) {
226         mHandler.post(() -> {
227             mPlayer.play();
228             Log.d(TAG, "Media played: " + surfaceId);
229         });
230     }
231 
232     @Override
onMediaPause(int surfaceId)233     public void onMediaPause(int surfaceId) {
234         mHandler.post(() -> {
235             if (mPlayer.isPlaying()) {
236                 mPlayer.pause();
237                 Log.d(TAG, "Media paused: " + surfaceId);
238             }
239         });
240     }
241 
242     @Override
onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis)243     public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
244         mHandler.post(() -> {
245             mPlayer.seekTo((int) timestampMillis);
246             Log.d(TAG, "Media seeked: " + surfaceId + ". Timestamp: " + timestampMillis);
247         });
248     }
249 
250     @Override
onConfigChange(@onNull Bundle config)251     public void onConfigChange(@NonNull Bundle config) {
252         final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
253                 mEnableLoop);
254         final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
255                 mMuteAudio);
256         mHandler.post(() -> {
257             if (mEnableLoop != enableLoop) {
258                 mEnableLoop = enableLoop;
259                 updateLoopingPlaybackStatus();
260             }
261 
262             if (mMuteAudio != muteAudio) {
263                 mMuteAudio = muteAudio;
264                 updateAudioMuteStatus();
265             }
266         });
267         Log.d(TAG, "Config changed. Updated config params: " + config);
268     }
269 
270     @Override
onDestroy()271     public void onDestroy() {
272         Log.d(TAG, "Surface controller destroyed.");
273     }
274 
createExoPlayer()275     private ExoPlayer createExoPlayer() {
276         // ProgressiveMediaFactory will be enough for video playback of videos on the device.
277         // This also reduces apk size.
278         ProgressiveMediaSource.Factory mediaSourceFactory = new ProgressiveMediaSource.Factory(
279                 () -> new ContentDataSource(mContext), MediaParserExtractorAdapter.FACTORY);
280 
281         return new ExoPlayer.Builder(mContext,
282                 new DefaultRenderersFactory(mContext),
283                 mediaSourceFactory,
284                 new DefaultTrackSelector(mContext),
285                 sLoadControl,
286                 DefaultBandwidthMeter.getSingletonInstance(mContext),
287                 new DefaultAnalyticsCollector(Clock.DEFAULT)).build();
288     }
289 
updateLoopingPlaybackStatus()290     private void updateLoopingPlaybackStatus() {
291         mPlayer.setRepeatMode(mEnableLoop ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
292     }
293 
updateAudioMuteStatus()294     private void updateAudioMuteStatus() {
295         if (mMuteAudio) {
296             mPlayer.setVolume(0f);
297         } else {
298             AudioManager audioManager = mContext.getSystemService(AudioManager.class);
299             if (audioManager == null) {
300                 Log.e(TAG, "Couldn't find AudioManager while trying to set volume,"
301                         + " unable to set volume");
302                 return;
303             }
304             mPlayer.setVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
305         }
306     }
307 }
308