mediaItems, int startIndex, long startPositionMs) {
80 | Log.d(TAG, "handleSetMediaItems");
81 | if (mediaItems.size() != 1) {
82 | Log.w(TAG, "handleSetMediaItems size not 1");
83 | return Futures.immediateVoidFuture();
84 | }
85 | MediaItem mediaItem = mediaItems.get(0);
86 | if (mediaItem == null
87 | || mediaItem.localConfiguration == null
88 | || mediaItem.mediaMetadata.extras == null
89 | || mediaItem.mediaMetadata.extras.keySet() == null) {
90 | Log.w(TAG, "mediaItem invalid");
91 | return Futures.immediateVoidFuture();
92 | }
93 | Log.d(TAG, "mediaItem:uri:" + mediaItem.localConfiguration.uri);
94 | for (String key : mediaItem.mediaMetadata.extras.keySet()) {
95 | Log.d(TAG, "mediaItem:" + key + ":" + mediaItem.mediaMetadata.extras.get(key));
96 | }
97 | state =
98 | state
99 | .buildUpon()
100 | .setPlaylist(
101 | ImmutableList.of(
102 | new MediaItemData.Builder(mediaItem).setMediaItem(mediaItem).build()))
103 | .build();
104 | return Futures.immediateVoidFuture();
105 | }
106 |
107 | @NonNull
108 | @Override
109 | protected ListenableFuture> handleStop() {
110 | Log.d(TAG, "handleStop");
111 | state = state.buildUpon().setPlaybackState(STATE_IDLE).build();
112 | stopStream();
113 | return Futures.immediateVoidFuture();
114 | }
115 |
116 | @NonNull
117 | @Override
118 | protected ListenableFuture> handleRelease() {
119 | Log.d(TAG, "handleRelease");
120 | handleStop();
121 | wifiLockManager.setEnabled(false);
122 | return Futures.immediateVoidFuture();
123 | }
124 |
125 | private void startStream() {
126 | if (isStreaming()) {
127 | Log.i(TAG, "startStream:already streaming");
128 | return;
129 | }
130 | if (!state.playWhenReady || state.playbackState != STATE_READY) {
131 | Log.i(TAG, "startStream:not ready");
132 | return;
133 | }
134 | if (state.playlist.get(0).mediaItem.mediaMetadata.extras == null) {
135 | Log.e(TAG, "startStream:no media selected");
136 | return;
137 | }
138 | Bundle mediaItemExtra = state.playlist.get(0).mediaItem.mediaMetadata.extras;
139 |
140 | workers =
141 | new WorkerThreadPair(
142 | context,
143 | this,
144 | mediaItemExtra.getString(MusicService.DATA_IP_ADDRESS),
145 | mediaItemExtra.getInt(MusicService.DATA_AUDIO_PORT, MusicService.DEFAULT_AUDIO_PORT),
146 | mediaItemExtra.getInt(MusicService.DATA_SAMPLE_RATE, MusicService.DEFAULT_SAMPLE_RATE),
147 | mediaItemExtra.getBoolean(MusicService.DATA_STEREO, MusicService.DEFAULT_STEREO),
148 | mediaItemExtra.getInt(MusicService.DATA_BUFFER_MS, MusicService.DEFAULT_BUFFER_MS),
149 | mediaItemExtra.getBoolean(MusicService.DATA_RETRY, MusicService.DEFAULT_RETRY),
150 | mediaItemExtra.getBoolean(
151 | MusicService.DATA_USE_PERFORMANCE_MODE, MusicService.DEFAULT_USE_PERFORMANCE_MODE),
152 | mediaItemExtra.getBoolean(
153 | MusicService.DATA_USE_MIN_BUFFER, MusicService.DEFAULT_USE_MIN_BUFFER));
154 |
155 | wifiLockManager.setStayAwake(true);
156 | }
157 |
158 | private void stopStream() {
159 | // we can also release the Wifi lock, if we're holding it
160 | wifiLockManager.setStayAwake(false);
161 |
162 | if (workers != null) {
163 | workers.stopAndInterrupt();
164 | workers = null;
165 | }
166 | }
167 |
168 | private boolean isStreaming() {
169 | return workers != null;
170 | }
171 |
172 | public void stopPlayback() {
173 | stopStream();
174 | invalidateState();
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/main/java/com/kaytat/simpleprotocolplayer/ThreadStoppable.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 The Android Open Source Project
3 | * Copyright (C) 2014 kaytat
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package com.kaytat.simpleprotocolplayer;
19 |
20 | class ThreadStoppable extends Thread {
21 | volatile boolean running = true;
22 |
23 | public void customStop() {
24 | running = false;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/kaytat/simpleprotocolplayer/WifiLockManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2020 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 | This was lifted from exoplayer.
18 | */
19 | package com.kaytat.simpleprotocolplayer;
20 |
21 | import android.content.Context;
22 | import android.net.wifi.WifiManager;
23 | import android.net.wifi.WifiManager.WifiLock;
24 | import android.util.Log;
25 | import androidx.annotation.Nullable;
26 |
27 | /**
28 | * Handles a {@link WifiLock}
29 | *
30 | * The handling of wifi locks requires the {@link android.Manifest.permission#WAKE_LOCK}
31 | * permission.
32 | */
33 | /* package */ final class WifiLockManager {
34 |
35 | private static final String TAG = "WifiLockManager";
36 | private static final String WIFI_LOCK_TAG = "ExoPlayer:WifiLockManager";
37 |
38 | @Nullable private final WifiManager wifiManager;
39 | @Nullable private WifiLock wifiLock;
40 | private boolean enabled;
41 | private boolean stayAwake;
42 |
43 | public WifiLockManager(Context context) {
44 | wifiManager =
45 | (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
46 | }
47 |
48 | /**
49 | * Sets whether to enable the usage of a {@link WifiLock}.
50 | *
51 | *
By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if
52 | * necessary. Disabling will release the wifi lock if held.
53 | *
54 | *
Enabling {@link WifiLock} requires the {@link android.Manifest.permission#WAKE_LOCK}.
55 | *
56 | * @param enabled True if the player should handle a {@link WifiLock}.
57 | */
58 | public void setEnabled(boolean enabled) {
59 | if (enabled && wifiLock == null) {
60 | if (wifiManager == null) {
61 | Log.w(TAG, "WifiManager is null, therefore not creating the WifiLock.");
62 | return;
63 | }
64 | wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WIFI_LOCK_TAG);
65 | wifiLock.setReferenceCounted(false);
66 | }
67 |
68 | this.enabled = enabled;
69 | updateWifiLock();
70 | }
71 |
72 | /**
73 | * Sets whether to acquire or release the {@link WifiLock}.
74 | *
75 | *
The wifi lock will not be acquired unless handling has been enabled through {@link
76 | * #setEnabled(boolean)}.
77 | *
78 | * @param stayAwake True if the player should acquire the {@link WifiLock}. False if it should
79 | * release.
80 | */
81 | public void setStayAwake(boolean stayAwake) {
82 | this.stayAwake = stayAwake;
83 | updateWifiLock();
84 | }
85 |
86 | private void updateWifiLock() {
87 | if (wifiLock == null) {
88 | return;
89 | }
90 |
91 | if (enabled && stayAwake) {
92 | wifiLock.acquire();
93 | } else {
94 | wifiLock.release();
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/com/kaytat/simpleprotocolplayer/WorkerThreadPair.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 The Android Open Source Project
3 | * Copyright (C) 2014 kaytat
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package com.kaytat.simpleprotocolplayer;
19 |
20 | import android.content.Context;
21 | import android.media.AudioAttributes;
22 | import android.media.AudioFormat;
23 | import android.media.AudioTrack;
24 | import android.os.Handler;
25 | import android.util.Log;
26 | import android.widget.Toast;
27 | import java.util.concurrent.ArrayBlockingQueue;
28 |
29 | /**
30 | * group everything belongs a stream together, makes multi stream easier including NetworkReadThread
31 | * BufferToAudioTrackThread and AudioTrack
32 | */
33 | public class WorkerThreadPair {
34 |
35 | interface StopPlaybackCallback {
36 | void stopPlayback();
37 | }
38 |
39 | private static final String TAG = WorkerThreadPair.class.getSimpleName();
40 |
41 | private final BufferToAudioTrackThread audioThread;
42 | private final NetworkReadThread networkThread;
43 | private final Context context;
44 | private final StopPlaybackCallback stopPlaybackCallback;
45 |
46 | final AudioTrack audioTrack;
47 |
48 | public WorkerThreadPair(
49 | Context context,
50 | StopPlaybackCallback stopPlaybackCallback,
51 | String serverAddr,
52 | int serverPort,
53 | int sampleRate,
54 | boolean stereo,
55 | int requestedBufferMs,
56 | boolean retry,
57 | boolean usePerformanceMode,
58 | boolean useMinBuffer) {
59 | this.context = context;
60 | this.stopPlaybackCallback = stopPlaybackCallback;
61 | int channelMask = stereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO;
62 |
63 | // Sanitize input, just in case
64 | if (sampleRate <= 0) {
65 | sampleRate = MusicService.DEFAULT_SAMPLE_RATE;
66 | }
67 |
68 | int audioTrackMinBuffer =
69 | AudioTrack.getMinBufferSize(sampleRate, channelMask, AudioFormat.ENCODING_PCM_16BIT);
70 | Log.d(TAG, "audioTrackMinBuffer:" + audioTrackMinBuffer);
71 |
72 | if (useMinBuffer) {
73 | bytesPerAudioPacket = calcMinBytesPerAudioPacket(stereo, audioTrackMinBuffer);
74 | } else {
75 | if (requestedBufferMs <= 5) {
76 | requestedBufferMs = MusicService.DEFAULT_BUFFER_MS;
77 | }
78 | bytesPerAudioPacket = calcBytesPerAudioPacket(sampleRate, stereo, requestedBufferMs);
79 | }
80 | Log.d(TAG, "useMinBuffer:" + useMinBuffer);
81 |
82 | // The agreement here is that audioTrack will be shutdown by the helper
83 | audioTrack = buildAudioTrack(sampleRate, channelMask, audioTrackMinBuffer, usePerformanceMode);
84 | Log.d(TAG, "usePerformanceMode:" + usePerformanceMode);
85 |
86 | audioThread = new BufferToAudioTrackThread(this, "audio:" + serverAddr + ":" + serverPort);
87 | networkThread =
88 | new NetworkReadThread(
89 | this, serverAddr, serverPort, retry, "net:" + serverAddr + ":" + serverPort);
90 |
91 | audioThread.start();
92 | networkThread.start();
93 | }
94 |
95 | static AudioTrack buildAudioTrack(
96 | int sampleRate, int channelMask, int audioTrackMinBuffer, boolean usePerformanceMode) {
97 | AudioTrack.Builder audioTrackBuilder =
98 | new AudioTrack.Builder()
99 | .setAudioAttributes(
100 | new AudioAttributes.Builder()
101 | .setUsage(AudioAttributes.USAGE_MEDIA)
102 | .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
103 | .build())
104 | .setAudioFormat(
105 | new AudioFormat.Builder()
106 | .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
107 | .setSampleRate(sampleRate)
108 | .setChannelMask(channelMask)
109 | .build())
110 | .setBufferSizeInBytes(audioTrackMinBuffer)
111 | .setTransferMode(AudioTrack.MODE_STREAM);
112 | if (usePerformanceMode) {
113 | audioTrackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
114 | }
115 | return audioTrackBuilder.build();
116 | }
117 |
118 | static int calcBytesPerAudioPacket(int sampleRate, boolean stereo, int requestedBufferMs) {
119 |
120 | // Assume 16 bits per sample
121 | int bytesPerSecond = sampleRate * 2;
122 | if (stereo) {
123 | bytesPerSecond *= 2;
124 | }
125 |
126 | int result = (bytesPerSecond * requestedBufferMs) / 1000;
127 |
128 | if (stereo) {
129 | result = (result + 3) & ~0x3;
130 | } else {
131 | result = (result + 1) & ~0x1;
132 | }
133 |
134 | Log.d(TAG, "calcBytesPerAudioPacket:bytes / second:" + bytesPerSecond);
135 | Log.d(TAG, "calcBytesPerAudioPacket:" + result);
136 |
137 | return result;
138 | }
139 |
140 | static int calcMinBytesPerAudioPacket(boolean stereo, int audioTrackMinBuffer) {
141 | int bytesPerAudioPacket;
142 |
143 | if (stereo) {
144 | bytesPerAudioPacket = (audioTrackMinBuffer + 3) & ~0x3;
145 | } else {
146 | bytesPerAudioPacket = (audioTrackMinBuffer + 1) & ~0x1;
147 | }
148 |
149 | Log.d(TAG, "calcMinBytesPerAudioPacket:audioTrackMinBuffer:" + audioTrackMinBuffer);
150 | Log.d(TAG, "calcMinBytesPerAudioPacket:" + bytesPerAudioPacket);
151 |
152 | return bytesPerAudioPacket;
153 | }
154 |
155 | public static final int NUM_PACKETS = 3;
156 |
157 | // The amount of data to read from the network before sending to AudioTrack
158 | final int bytesPerAudioPacket;
159 |
160 | final ArrayBlockingQueue dataQueue = new ArrayBlockingQueue<>(NUM_PACKETS);
161 |
162 | public void stopAndInterrupt() {
163 | for (ThreadStoppable it : new ThreadStoppable[] {audioThread, networkThread}) {
164 |
165 | try {
166 | it.customStop();
167 | it.interrupt();
168 |
169 | // Do not join since this can take some time. The
170 | // workers should be able to shutdown independently.
171 | // t.join();
172 | } catch (Exception e) {
173 | Log.e(TAG, "join exception:" + e);
174 | }
175 | }
176 | }
177 |
178 | public void brokenShutdown() {
179 | // Broke out of loop unexpectedly. Shutdown.
180 | Handler h = new Handler(context.getMainLooper());
181 | Runnable r =
182 | () -> {
183 | Toast.makeText(context.getApplicationContext(), "Unable to stream", Toast.LENGTH_SHORT)
184 | .show();
185 | stopPlaybackCallback.stopPlayback();
186 | };
187 | h.post(r);
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/ic_stat_playing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-hdpi/ic_stat_playing.png
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-hdpi/play.png
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/play_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-hdpi/play_pressed.png
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-hdpi/stop.png
--------------------------------------------------------------------------------
/src/main/res/drawable-hdpi/stop_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-hdpi/stop_pressed.png
--------------------------------------------------------------------------------
/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/drawable-mdpi/ic_stat_playing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-mdpi/ic_stat_playing.png
--------------------------------------------------------------------------------
/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/drawable-xhdpi/ic_stat_playing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaytat/SimpleProtocolPlayer/f7120412307d4231060ffee4d3a0dfd4258bb5d7/src/main/res/drawable-xhdpi/ic_stat_playing.png
--------------------------------------------------------------------------------
/src/main/res/drawable/btn_play.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/res/drawable/btn_stop.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
16 |
20 |
21 |
26 |
27 |
31 |
32 |
38 |
39 |
47 |
48 |
59 |
60 |
61 |
65 |
66 |
72 |
73 |
81 |
82 |
94 |
95 |
96 |
100 |
101 |
107 |
108 |
116 |
117 |
124 |
125 |
126 |
130 |
131 |
137 |
138 |
146 |
147 |
154 |
155 |
156 |
160 |
161 |
167 |
168 |
176 |
177 |
188 |
189 |
190 |
194 |
195 |
201 |
202 |
210 |
211 |
218 |
219 |
220 |
224 |
225 |
231 |
232 |
240 |
241 |
248 |
249 |
250 |
254 |
255 |
261 |
262 |
270 |
271 |
278 |
279 |
280 |
284 |
285 |
291 |
292 |
300 |
301 |
309 |
310 |
311 |
317 |
318 |
326 |
327 |
335 |
336 |
337 |
338 |
339 |
340 |
--------------------------------------------------------------------------------
/src/main/res/layout/notice.xml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
18 |
19 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/src/main/res/menu/actions.xml:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | Simple Protocol Player
20 | IP Address / Hostname
21 | IP Address / Hostname
22 | Audio Port
23 | Audio Port
24 | 12345
25 | Notice
26 | Notice
27 | Sample Rate
28 | Mono/Stereo
29 |
30 | - 11025
31 | - 12000
32 | - 22050
33 | - 24000
34 | - 44100 (Default)
35 | - 48000
36 |
37 |
38 | - Mono
39 | - Stereo (Default)
40 |
41 |
42 | Stereo
43 | Buffer size (in ms)
44 | Buffer size
45 | Enable network retries
46 | Performance mode
47 | Min AudioTrack buffer
48 | Media3
49 | Error
50 |
51 |
--------------------------------------------------------------------------------
/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------