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