Skip to contentMethod: changeTrack(AudioFile)
1: /*
2: * *********************************************************************************************************************
3: *
4: * blueMarine II: Semantic Media Centre
5: * http://tidalwave.it/projects/bluemarine2
6: *
7: * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/bluemarine2-src
23: * git clone https://github.com/tidalwave-it/bluemarine2-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.bluemarine2.ui.audio.renderer.impl;
28:
29: import javax.annotation.Nonnull;
30: import javax.annotation.PostConstruct;
31: import javax.inject.Inject;
32: import java.time.Duration;
33: import java.util.stream.Collectors;
34: import javafx.beans.property.ObjectProperty;
35: import javafx.beans.value.ChangeListener;
36: import javafx.beans.value.ObservableValue;
37: import javafx.application.Platform;
38: import it.tidalwave.util.annotation.VisibleForTesting;
39: import it.tidalwave.role.ui.UserAction;
40: import it.tidalwave.messagebus.annotation.ListensTo;
41: import it.tidalwave.messagebus.annotation.SimpleMessageSubscriber;
42: import it.tidalwave.bluemarine2.model.MediaItem.Metadata;
43: import it.tidalwave.bluemarine2.model.PlayList;
44: import it.tidalwave.bluemarine2.model.audio.AudioFile;
45: import it.tidalwave.bluemarine2.ui.commons.OnDeactivate;
46: import it.tidalwave.bluemarine2.ui.commons.RenderAudioFileRequest;
47: import it.tidalwave.bluemarine2.ui.audio.renderer.AudioRendererPresentation;
48: import it.tidalwave.bluemarine2.ui.audio.renderer.MediaPlayer;
49: import it.tidalwave.bluemarine2.ui.audio.renderer.MediaPlayer.Status;
50: import lombok.extern.slf4j.Slf4j;
51: import static it.tidalwave.util.PropertyWrapper.wrap;
52: import static it.tidalwave.role.ui.Displayable._Displayable_;
53: import static it.tidalwave.bluemarine2.util.Formatters.*;
54: import static it.tidalwave.bluemarine2.model.MediaItem.Metadata.*;
55: import static it.tidalwave.bluemarine2.ui.audio.renderer.MediaPlayer.Status.PLAYING;
56:
57: /***********************************************************************************************************************
58: *
59: * The Control of the {@link AudioRendererPresentation}.
60: *
61: * @stereotype Control
62: *
63: * @author Fabrizio Giudici
64: *
65: **********************************************************************************************************************/
66: @SimpleMessageSubscriber @Slf4j
67: public class DefaultAudioRendererPresentationControl
68: {
69: @Inject
70: private AudioRendererPresentation presentation;
71:
72: @Inject
73: private MediaPlayer mediaPlayer;
74:
75: private final AudioRendererPresentation.Properties properties = new AudioRendererPresentation.Properties();
76:
77: private Duration duration = Duration.ZERO;
78:
79: private PlayList<AudioFile> playList = PlayList.empty();
80:
81: // Discriminates a forced stop from media player just terminating
82: private boolean stopped;
83:
84: private final UserAction prevAction = UserAction.of(() -> changeTrack(playList.previous().get()));
85:
86: private final UserAction nextAction = UserAction.of(() -> changeTrack(playList.next().get()));
87:
88: private final UserAction rewindAction = UserAction.of(() -> mediaPlayer.rewind());
89:
90: private final UserAction fastForwardAction = UserAction.of(() -> mediaPlayer.fastForward());
91:
92: private final UserAction pauseAction = UserAction.of(() -> mediaPlayer.pause());
93:
94: private final UserAction playAction = UserAction.of(this::play);
95:
96: private final UserAction stopAction = UserAction.of(this::stop);
97:
98: // FIXME: use expression binding
99: // e.g. properties.progressProperty().bind(mediaPlayer.playTimeProperty().asDuration().dividedBy/duration));
100: // FIXME: weak, remove previous listeners
101: private final ChangeListener<Duration> l =
102: (ObservableValue<? extends Duration> observable,
103: Duration oldValue,
104: Duration newValue) ->
105: {
106: // FIXME: the control shouldn't mess with JavaFX stuff
107: Platform.runLater(() ->
108: {
109: properties.playTimeProperty().setValue(format(newValue));
110: properties.progressProperty().setValue((double)newValue.toMillis() / duration.toMillis());
111: });
112: };
113:
114: /*******************************************************************************************************************
115: *
116: *
117: ******************************************************************************************************************/
118: @PostConstruct
119: @VisibleForTesting void initialize()
120: {
121: presentation.bind(properties,
122: prevAction, rewindAction, stopAction, pauseAction, playAction, fastForwardAction, nextAction);
123: }
124:
125: /*******************************************************************************************************************
126: *
127: *
128: ******************************************************************************************************************/
129: @VisibleForTesting void onRenderAudioFileRequest (@ListensTo @Nonnull final RenderAudioFileRequest request)
130: throws MediaPlayer.Exception
131: {
132: log.info("onRenderAudioFileRequest({})", request);
133:
134: playList = request.getPlayList();
135: setAudioFile(playList.getCurrentItem().get());
136: bindMediaPlayer();
137: presentation.showUp(this);
138: presentation.focusOnPlayButton();
139: }
140:
141: /*******************************************************************************************************************
142: *
143: *
144: ******************************************************************************************************************/
145: @OnDeactivate
146: @VisibleForTesting OnDeactivate.Result onDeactivate()
147: throws MediaPlayer.Exception
148: {
149: stop();
150: unbindMediaPlayer();
151: playList = PlayList.empty();
152: return OnDeactivate.Result.PROCEED;
153: }
154:
155: /*******************************************************************************************************************
156: *
157: *
158: ******************************************************************************************************************/
159: private void setAudioFile (@Nonnull final AudioFile audioFile)
160: throws MediaPlayer.Exception
161: {
162: log.info("setAudioFile({})", audioFile);
163: final Metadata metadata = audioFile.getMetadata();
164: log.info(">>>> metadata: {}", metadata);
165:
166: // FIXME: the control shouldn't mess with JavaFX stuff
167: // FIXME: this performs some (short) queries that are executed in the JavaFX thread
168: Platform.runLater(() ->
169: {
170: properties.titleProperty().setValue(metadata.get(TITLE).orElse(""));
171: properties.artistProperty().setValue(audioFile.findMakers().stream()
172: .map(maker -> maker.as(_Displayable_).getDisplayName())
173: .collect(Collectors.joining(", ")));
174: properties.composerProperty().setValue(audioFile.findComposers().stream()
175: .map(composer -> composer.as(_Displayable_).getDisplayName())
176: .collect(Collectors.joining(", ")));
177: duration = metadata.get(DURATION).orElse(Duration.ZERO);
178: properties.durationProperty().setValue(format(duration));
179: properties.folderNameProperty().setValue(
180: audioFile.getRecord().map(record -> record.as(_Displayable_).getDisplayName()).orElse(""));
181: properties.nextTrackProperty().setValue(
182: ((playList.getSize() == 1) ? "" : String.format("%d / %d", playList.getIndex() + 1, playList.getSize()) +
183: playList.peekNext().map(t -> " - Next track: " + t.getMetadata().get(TITLE).orElse("")).orElse("")));
184: });
185:
186: mediaPlayer.setMediaItem(audioFile);
187: }
188:
189: /*******************************************************************************************************************
190: *
191: *
192: *
193: ******************************************************************************************************************/
194: private void onMediaPlayerStarted()
195: {
196: log.info("onMediaPlayerStarted()");
197: // presentation.focusOnStopButton();
198: }
199:
200: /*******************************************************************************************************************
201: *
202: *
203: *
204: ******************************************************************************************************************/
205: private void onMediaPlayerStopped()
206: {
207: log.info("onMediaPlayerStopped()");
208:
209: if (!stopped)
210: {
211: presentation.focusOnPlayButton();
212: }
213:
214: if (!stopped && playList.hasNext())
215: {
216: // FIXME: check whether the disk is not gapless, and eventually pause
217: try
218: {
219: setAudioFile(playList.next().get());
220: play();
221: }
222: catch (MediaPlayer.Exception e)
223: {
224: log.error("", e);
225: }
226: }
227: }
228:
229: /*******************************************************************************************************************
230: *
231: *
232: *
233: ******************************************************************************************************************/
234: private void play()
235: throws MediaPlayer.Exception
236: {
237: stopped = false;
238: mediaPlayer.play();
239: }
240:
241: /*******************************************************************************************************************
242: *
243: *
244: *
245: ******************************************************************************************************************/
246: private void stop()
247: throws MediaPlayer.Exception
248: {
249: stopped = true;
250: mediaPlayer.stop();
251: }
252:
253: /*******************************************************************************************************************
254: *
255: *
256: *
257: ******************************************************************************************************************/
258: private void changeTrack (@Nonnull final AudioFile audioFile)
259: throws MediaPlayer.Exception
260: {
261: final boolean wasPlaying = mediaPlayer.statusProperty().get().equals(PLAYING);
262:
263:• if (wasPlaying)
264: {
265: stop();
266: }
267:
268: setAudioFile(audioFile);
269:
270:• if (wasPlaying)
271: {
272: play();
273: }
274: }
275:
276: /*******************************************************************************************************************
277: *
278: * Binds to the {@link MediaPlayer}.
279: *
280: ******************************************************************************************************************/
281: @VisibleForTesting void bindMediaPlayer()
282: {
283: log.debug("bindMediaPlayer()");
284: final ObjectProperty<Status> status = mediaPlayer.statusProperty();
285: wrap(stopAction.enabled()).bind(status.isEqualTo(PLAYING));
286: wrap(pauseAction.enabled()).bind(status.isEqualTo(PLAYING));
287: wrap(playAction.enabled()).bind(status.isNotEqualTo(PLAYING));
288: wrap(prevAction.enabled()).bind(playList.hasPreviousProperty());
289: wrap(nextAction.enabled()).bind(playList.hasNextProperty());
290: mediaPlayer.playTimeProperty().addListener(l);
291:
292: status.addListener((observable, oldValue, newValue) ->
293: {
294: switch (newValue)
295: {
296: case STOPPED:
297: onMediaPlayerStopped();
298: break;
299:
300: case PLAYING:
301: onMediaPlayerStarted();
302: break;
303: }
304: });
305: }
306:
307: /*******************************************************************************************************************
308: *
309: * Unbinds from the {@link MediaPlayer}.
310: *
311: ******************************************************************************************************************/
312: @VisibleForTesting void unbindMediaPlayer()
313: {
314: log.debug("unbindMediaPlayer()");
315: wrap(stopAction.enabled()).unbind();
316: wrap(pauseAction.enabled()).unbind();
317: wrap(playAction.enabled()).unbind();
318: wrap(prevAction.enabled()).unbind();
319: wrap(nextAction.enabled()).unbind();
320: mediaPlayer.playTimeProperty().removeListener(l);
321: }
322: }