1 /*
2  * Copyright (C) 2023 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.sdksandboxcode_1;
18 
19 import android.content.Context;
20 import android.net.Uri;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.util.Log;
24 import android.view.View;
25 
26 import androidx.media3.common.AudioAttributes;
27 import androidx.media3.common.C;
28 import androidx.media3.common.MediaItem;
29 import androidx.media3.common.PlaybackException;
30 import androidx.media3.common.Player;
31 import androidx.media3.exoplayer.ExoPlayer;
32 import androidx.media3.ui.PlayerView;
33 
34 import java.util.WeakHashMap;
35 import java.util.concurrent.atomic.AtomicLong;
36 
37 /** Create PlayerView with Player and controlling playback based on host activity lifecycle. */
38 class PlayerViewProvider {
39 
40     private static final String TAG = "PlayerViewProvider";
41 
42     private final Handler mMainHandler = new Handler(Looper.getMainLooper());
43 
44     private final WeakHashMap<PlayerView, PlayerState> mCreatedViews = new WeakHashMap<>();
45 
46     private final AtomicLong mLastCreatedViewId = new AtomicLong(0);
47 
48     private boolean mHostActivityStarted = true;
49 
createPlayerView(Context windowContext, String videoUrl)50     public View createPlayerView(Context windowContext, String videoUrl) {
51         final long viewId = mLastCreatedViewId.incrementAndGet();
52         final PlayerViewLogger logger = new PlayerViewLogger(viewId);
53         logger.info("Creating PlayerView");
54 
55         final PlayerView view = new PlayerView(windowContext);
56         final PlayerState playerState = new PlayerState(windowContext, logger, videoUrl);
57 
58         mMainHandler.post(
59                 () -> {
60                     mCreatedViews.put(view, playerState);
61                     if (mHostActivityStarted) {
62                         final Player player = playerState.initializePlayer();
63                         view.setPlayer(player);
64                     }
65                 });
66 
67         return view;
68     }
69 
onHostActivityStarted()70     public void onHostActivityStarted() {
71         mMainHandler.post(
72                 () -> {
73                     mHostActivityStarted = true;
74                     mCreatedViews.forEach(
75                             (view, state) -> {
76                                 if (view.getPlayer() == null) {
77                                     final Player player = state.initializePlayer();
78                                     view.setPlayer(player);
79                                     view.onResume();
80                                 }
81                             });
82                 });
83     }
84 
onHostActivityStopped()85     public void onHostActivityStopped() {
86         mMainHandler.post(
87                 () -> {
88                     mHostActivityStarted = false;
89                     mCreatedViews.forEach(
90                             (view, state) -> {
91                                 view.onPause();
92                                 state.releasePlayer();
93                                 view.setPlayer(null);
94                             });
95                 });
96     }
97 
98     private static final class PlayerState {
99         private final Context mContext;
100         private final PlayerViewLogger mLogger;
101         private final MediaItem mMediaItem;
102         private ExoPlayer mPlayer;
103         private boolean mAutoPlay;
104         private long mAutoPlayPosition;
105 
PlayerState(Context context, PlayerViewLogger logger, String videoUrl)106         private PlayerState(Context context, PlayerViewLogger logger, String videoUrl) {
107             mContext = context;
108             mLogger = logger;
109             mMediaItem = MediaItem.fromUri(Uri.parse(videoUrl));
110             mAutoPlayPosition = C.TIME_UNSET;
111             mAutoPlay = true;
112         }
113 
initializePlayer()114         private Player initializePlayer() {
115             mLogger.info("Initializing Player");
116             if (mPlayer != null) {
117                 return mPlayer;
118             }
119 
120             AudioAttributes audioAttributes =
121                     new AudioAttributes.Builder()
122                             .setUsage(C.USAGE_MEDIA)
123                             .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
124                             .build();
125 
126             mPlayer =
127                     new ExoPlayer.Builder(mContext)
128                             .setAudioAttributes(audioAttributes, true)
129                             .build();
130             mPlayer.addListener(new PlayerListener(mPlayer, mLogger));
131             mPlayer.setPlayWhenReady(mAutoPlay);
132             mPlayer.setMediaItem(mMediaItem);
133             boolean hasStartPosition = mAutoPlayPosition != C.TIME_UNSET;
134             if (hasStartPosition) {
135                 mPlayer.seekTo(0, mAutoPlayPosition);
136             }
137             mPlayer.prepare();
138 
139             return mPlayer;
140         }
141 
releasePlayer()142         private void releasePlayer() {
143             mLogger.info("Releasing Player");
144             if (mPlayer == null) {
145                 return;
146             }
147 
148             mAutoPlay = mPlayer.getPlayWhenReady();
149             mAutoPlayPosition = mPlayer.getContentPosition();
150 
151             mPlayer.release();
152             mPlayer = null;
153         }
154     }
155 
156     private static class PlayerListener implements Player.Listener {
157 
158         private final Player mPlayer;
159 
160         private final PlayerViewLogger mLogger;
161 
PlayerListener(Player player, PlayerViewLogger logger)162         private PlayerListener(Player player, PlayerViewLogger logger) {
163             mPlayer = player;
164             mLogger = logger;
165         }
166 
167         @Override
onIsLoadingChanged(boolean isLoading)168         public void onIsLoadingChanged(boolean isLoading) {
169             mLogger.info("Player onIsLoadingChanged, isLoading = " + isLoading);
170         }
171 
172         @Override
onPlaybackStateChanged(int playbackState)173         public void onPlaybackStateChanged(int playbackState) {
174             mLogger.info("Player onPlaybackStateChanged, playbackState = " + playbackState);
175             if (playbackState == Player.STATE_READY) {
176                 // Unmute at new playback
177                 mPlayer.setVolume(1);
178             }
179         }
180 
181         @Override
onIsPlayingChanged(boolean isPlaying)182         public void onIsPlayingChanged(boolean isPlaying) {
183             mLogger.info("Player onIsPlayingChanged, isPlaying = " + isPlaying);
184             if (!isPlaying) {
185                 // For testing, mute the video when it is paused until end of current playback.
186                 mPlayer.setVolume(0);
187             }
188         }
189 
190         @Override
onPlayerError(PlaybackException error)191         public void onPlayerError(PlaybackException error) {
192             mLogger.error(error);
193         }
194     }
195 
196     private static final class PlayerViewLogger {
197 
198         private final long mViewId;
199 
PlayerViewLogger(long viewId)200         private PlayerViewLogger(long viewId) {
201             mViewId = viewId;
202         }
203 
info(String message)204         public void info(String message) {
205             Log.i(TAG, "[PlayerView#" + mViewId + "] " + message);
206         }
207 
error(Exception exception)208         public void error(Exception exception) {
209             Log.e(TAG, "[PlayerView#" + mViewId + "] " + exception.getMessage(), exception);
210         }
211     }
212 }
213