1/*
2 * Copyright (C) 2017 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
17package com.googlecode.android_scripting.facade.media;
18
19import android.app.Service;
20import android.media.MediaPlayer;
21import android.net.Uri;
22
23import com.googlecode.android_scripting.facade.EventFacade;
24import com.googlecode.android_scripting.facade.FacadeManager;
25import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
26import com.googlecode.android_scripting.rpc.Rpc;
27import com.googlecode.android_scripting.rpc.RpcDefault;
28import com.googlecode.android_scripting.rpc.RpcParameter;
29
30import java.util.HashMap;
31import java.util.Hashtable;
32import java.util.Map;
33import java.util.Set;
34import java.util.Map.Entry;
35
36/**
37 * This facade exposes basic mediaPlayer functionality. <br>
38 * <br>
39 * <b>Usage Notes:</b><br>
40 * mediaPlayerFacade maintains a list of media streams, identified by a user supplied tag. If the
41 * tag is null or blank, this tag defaults to "default"<br>
42 * Basic operation is: mediaPlayOpen("file:///sdcard/MP3/sample.mp3","mytag",true)<br>
43 * This will look for a media file at /sdcard/MP3/sample.mp3. Other urls should work. If the file
44 * exists and is playable, this will return a true otherwise it will return a false. <br>
45 * If play=true, then the media file will play immediately, otherwise it will wait for a
46 * {@link #mediaPlayStart mediaPlayerStart} command. <br>
47 * When done with the resource, use {@link #mediaPlayClose mediaPlayClose} <br>
48 * You can get information about the loaded media with {@link #mediaPlayInfo mediaPlayInfo} This
49 * returns a map with the following elements:
50 * <ul>
51 * <li>"tag" - user supplied tag identifying this mediaPlayer.
52 * <li>"loaded" - true if loaded, false if not. If false, no other elements are returned.
53 * <li>"duration" - length of the media in milliseconds.
54 * <li>"position" - current position of playback in milliseconds. Controlled by
55 * {@link #mediaPlaySeek mediaPlaySeek}
56 * <li>"isplaying" - shows whether media is playing. Controlled by {@link #mediaPlayPause
57 * mediaPlayPause} and {@link #mediaPlayStart mediaPlayStart}
58 * <li>"url" - the url used to open this media.
59 * <li>"looping" - whether media will loop. Controlled by {@link #mediaPlaySetLooping
60 * mediaPlaySetLooping}
61 * </ul>
62 * <br>
63 * You can use {@link #mediaPlayList mediaPlayList} to get a list of the loaded tags. <br>
64 * {@link #mediaIsPlaying mediaIsPlaying} will return true if the media is playing.<br>
65 * <b>Events:</b><br>
66 * A playing media will throw a <b>"media"</b> event on completion. NB: In remote mode, a media file
67 * will continue playing after the script has finished unless an explicit {@link #mediaPlayClose
68 * mediaPlayClose} event is called.
69 *
70 */
71
72public class MediaPlayerFacade extends RpcReceiver implements MediaPlayer.OnCompletionListener {
73
74    private final Service mService;
75    static private final Map<String, MediaPlayer> mPlayers = new Hashtable<String, MediaPlayer>();
76    static private final Map<String, String> mUrls = new Hashtable<String, String>();
77
78    private final EventFacade mEventFacade;
79
80    public MediaPlayerFacade(FacadeManager manager) {
81        super(manager);
82        mService = manager.getService();
83        mEventFacade = manager.getReceiver(EventFacade.class);
84    }
85
86    private String getDefault(String tag) {
87        return (tag == null || tag.equals("")) ? "default" : tag;
88    }
89
90    private MediaPlayer getPlayer(String tag) {
91        tag = getDefault(tag);
92        return mPlayers.get(tag);
93    }
94
95    private String getUrl(String tag) {
96        tag = getDefault(tag);
97        return mUrls.get(tag);
98    }
99
100    private void putMp(String tag, MediaPlayer player, String url) {
101        tag = getDefault(tag);
102        mPlayers.put(tag, player);
103        mUrls.put(tag, url);
104    }
105
106    private void removeMp(String tag) {
107        tag = getDefault(tag);
108        MediaPlayer player = mPlayers.get(tag);
109        if (player != null) {
110            player.stop();
111            player.release();
112        }
113        mPlayers.remove(tag);
114        mUrls.remove(tag);
115    }
116
117    @Rpc(description = "Open a media file", returns = "true if play successful")
118    public synchronized boolean mediaPlayOpen(@RpcParameter(name = "url",
119                                                            description = "url of media resource")
120    String url, @RpcParameter(name = "tag", description = "string identifying resource")
121    @RpcDefault(value = "default")
122    String tag, @RpcParameter(name = "play", description = "start playing immediately")
123    @RpcDefault(value = "true")
124    Boolean play) {
125        removeMp(tag);
126        MediaPlayer player = getPlayer(tag);
127        player = MediaPlayer.create(mService, Uri.parse(url));
128        if (player != null) {
129            putMp(tag, player, url);
130            player.setOnCompletionListener(this);
131            if (play) {
132                player.start();
133            }
134        }
135        return player != null;
136    }
137
138    @Rpc(description = "pause playing media file", returns = "true if successful")
139    public synchronized boolean mediaPlayPause(
140            @RpcParameter(name = "tag", description = "string identifying resource")
141            @RpcDefault(value = "default")
142            String tag) {
143        MediaPlayer player = getPlayer(tag);
144        if (player == null) {
145            return false;
146        }
147        player.pause();
148        return true;
149    }
150
151    @Rpc(description = "Start playing media file.", returns = "true if successful")
152    public synchronized boolean mediaPlayStart(
153            @RpcParameter(name = "tag", description = "string identifying resource")
154            @RpcDefault(value = "default")
155            String tag) {
156        MediaPlayer player = getPlayer(tag);
157        if (player == null) {
158            return false;
159        }
160        player.start();
161        return mediaIsPlaying(tag);
162    }
163
164    @Rpc(description = "Stop playing media file.", returns = "true if successful")
165    public synchronized boolean mediaPlayStop(
166            @RpcParameter(name = "tag", description = "string identifying resource")
167            @RpcDefault(value = "default")
168            String tag) {
169        MediaPlayer player = getPlayer(tag);
170        if (player == null) {
171            return false;
172        }
173        player.stop();
174        return !mediaIsPlaying(tag) && player.getCurrentPosition() == 0;
175    }
176
177    @Rpc(description = "Stop all players.")
178    public synchronized void mediaPlayStopAll() {
179        for (MediaPlayer p : mPlayers.values()) {
180            p.stop();
181        }
182    }
183
184    @Rpc(description = "Seek To Position", returns = "New Position (in ms)")
185    public synchronized int mediaPlaySeek(@RpcParameter(name = "msec",
186                                                        description = "Position in millseconds")
187    Integer msec, @RpcParameter(name = "tag", description = "string identifying resource")
188    @RpcDefault(value = "default")
189    String tag) {
190        MediaPlayer player = getPlayer(tag);
191        if (player == null) {
192            return 0;
193        }
194        player.seekTo(msec);
195        return player.getCurrentPosition();
196    }
197
198    @Rpc(description = "Close media file", returns = "true if successful")
199    public synchronized boolean mediaPlayClose(
200            @RpcParameter(name = "tag", description = "string identifying resource")
201            @RpcDefault(value = "default")
202            String tag) throws Exception {
203        if (!mPlayers.containsKey(tag)) {
204            return false;
205        }
206        removeMp(tag);
207        return true;
208    }
209
210    @Rpc(description = "Checks if media file is playing.", returns = "true if playing")
211    public synchronized boolean mediaIsPlaying(
212            @RpcParameter(name = "tag", description = "string identifying resource")
213            @RpcDefault(value = "default")
214            String tag) {
215        MediaPlayer player = getPlayer(tag);
216        return (player == null) ? false : player.isPlaying();
217    }
218
219    @Rpc(description = "Information on current media", returns = "Media Information")
220    public synchronized Map<String, Object> mediaPlayGetInfo(
221            @RpcParameter(name = "tag", description = "string identifying resource")
222            @RpcDefault(value = "default")
223            String tag) {
224        Map<String, Object> result = new HashMap<String, Object>();
225        MediaPlayer player = getPlayer(tag);
226        result.put("tag", getDefault(tag));
227        if (player == null) {
228            result.put("loaded", false);
229        } else {
230            result.put("loaded", true);
231            result.put("duration", player.getDuration());
232            result.put("position", player.getCurrentPosition());
233            result.put("isplaying", player.isPlaying());
234            result.put("url", getUrl(tag));
235            result.put("looping", player.isLooping());
236        }
237        return result;
238    }
239
240    @Rpc(description = "Lists currently loaded media", returns = "List of Media Tags")
241    public Set<String> mediaPlayList() {
242        return mPlayers.keySet();
243    }
244
245    @Rpc(description = "Set Looping", returns = "True if successful")
246    public synchronized boolean mediaPlaySetLooping(@RpcParameter(name = "enabled")
247    @RpcDefault(value = "true")
248    Boolean enabled, @RpcParameter(name = "tag", description = "string identifying resource")
249    @RpcDefault(value = "default")
250    String tag) {
251        MediaPlayer player = getPlayer(tag);
252        if (player == null) {
253            return false;
254        }
255        player.setLooping(enabled);
256        return true;
257    }
258
259    @Rpc(description = "Checks if media file is playing.", returns = "true if playing")
260    public synchronized void mediaSetNext(
261            @RpcParameter(name = "tag", description = "string identifying resource")
262            @RpcDefault(value = "default")
263            String tag,
264            @RpcParameter(name = "next", description = "tag of the next track to play.")
265            String next) {
266        MediaPlayer player = getPlayer(tag);
267        MediaPlayer nPlayer = getPlayer(next);
268        if (player == null) {
269            throw new NullPointerException("Non-existent player tag " + tag);
270        }
271        if (nPlayer == null) {
272            throw new NullPointerException("Non-existent player tag " + next);
273        }
274        player.setNextMediaPlayer(nPlayer);
275    }
276
277    @Override
278    public synchronized void shutdown() {
279        for (String key : mPlayers.keySet()) {
280            MediaPlayer player = mPlayers.get(key);
281            if (player != null) {
282                player.stop();
283                player.release();
284                player = null;
285            }
286        }
287        mPlayers.clear();
288        mUrls.clear();
289    }
290
291    @Override
292    public void onCompletion(MediaPlayer player) {
293        String tag = getTag(player);
294        if (tag != null) {
295            Map<String, Object> data = new HashMap<String, Object>();
296            data.put("action", "complete");
297            data.put("tag", tag);
298            mEventFacade.postEvent("media", data);
299        }
300    }
301
302    private String getTag(MediaPlayer player) {
303        for (Entry<String, MediaPlayer> m : mPlayers.entrySet()) {
304            if (m.getValue() == player) {
305                return m.getKey();
306            }
307        }
308        return null;
309    }
310}
311