001package org.intellimate.izou.sdk.frameworks.music.player.template;
002
003import org.intellimate.izou.events.EventListenerModel;
004import org.intellimate.izou.events.EventModel;
005import org.intellimate.izou.identification.Identifiable;
006import org.intellimate.izou.identification.Identification;
007import org.intellimate.izou.identification.IdentificationManager;
008import org.intellimate.izou.resource.ResourceBuilderModel;
009import org.intellimate.izou.resource.ResourceModel;
010import org.intellimate.izou.sdk.Context;
011import org.intellimate.izou.sdk.frameworks.common.resources.SelectorResource;
012import org.intellimate.izou.sdk.frameworks.music.Capabilities;
013import org.intellimate.izou.sdk.frameworks.music.events.PlayerCommand;
014import org.intellimate.izou.sdk.frameworks.music.events.PlayerError;
015import org.intellimate.izou.sdk.frameworks.music.events.StartMusicRequest;
016import org.intellimate.izou.sdk.frameworks.music.events.StopMusic;
017import org.intellimate.izou.sdk.frameworks.music.player.*;
018import org.intellimate.izou.sdk.frameworks.music.resources.*;
019import org.intellimate.izou.sdk.frameworks.permanentSoundOutput.events.MuteEvent;
020import org.intellimate.izou.sdk.frameworks.permanentSoundOutput.events.StopEvent;
021import org.intellimate.izou.sdk.frameworks.permanentSoundOutput.events.UnMuteEvent;
022import org.intellimate.izou.sdk.frameworks.presence.events.LeavingEvent;
023import org.intellimate.izou.sdk.output.OutputPlugin;
024
025import java.util.*;
026import java.util.concurrent.CompletableFuture;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.locks.Condition;
029import java.util.concurrent.locks.Lock;
030import java.util.concurrent.locks.ReentrantLock;
031import java.util.function.Consumer;
032import java.util.function.Function;
033
034/**
035 * use this class to actually play music.
036 * <p>
037 * <b>How to use it:</b><br>
038 * Usage is pretty straightforward. Extend this class an override the abstract methods.<br>
039 *  - To register further commands, use the getCommandHandler() method in the constructor.<br>
040 *  - To start playing, use the PlayerController.startPlaying() methods, never call the play() method directly!
041 * Also, don't call the stopSound() method directly, use stopMusicPlayback().
042 * All basic methods are implemented in this class,but to code special behaviour fell free to explore the other
043 * classes and interfaces.<br>
044 * Note: the playback automatically stops when the user left.<br>
045 * </p>
046 * @author LeanderK
047 * @version 1.0
048 */
049public abstract class Player<T> extends OutputPlugin<T> implements MusicProvider, ResourceBuilderModel, MusicHelper, EventListenerModel {
050    private Playlist playlist = new Playlist(new ArrayList<>());
051    private Volume volume = Volume.createVolume(50).orElse(null);
052    private Progress progress = new Progress(0,0);
053    private PlaybackState playbackState = PlaybackState.PLAY;
054    private final Capabilities capabilities;
055    private CompletableFuture<?> playingThread = null;
056    private final boolean runsInPlay;
057    private boolean isRunning = false;
058    private boolean isPlaying = false;
059    private final List<Identifiable> activators;
060    private final CommandHandler commandHandler;
061    private final InformationProvider informationProvider;
062    final boolean isUsingJava;
063    private final Lock lock = new ReentrantLock();
064    Condition blockRequest = null;
065
066    /**
067     * creates a new output-plugin with a new id
068     *  @param context context
069     * @param id      the id of the new output-plugin. The ID of the InformationProvider is also based on this id
070     *                (id + ".InformationProvider")
071     * @param runsInPlay whether the termination of the play method should be treated as the termination the
072     *                   music (STOP not PAUSE)
073     * @param activators the activators which are able to start the Player if the Player is not able to start from
074     *                   request from other addons
075     * @param providesTrackInfo whether the Player provides TrackInfo
076     * @param playbackShuffle whether the player is able to provide the info that the playback is shuffling
077     * @param playbackRepeat whether the player is able to provide the info that the playback is repeating
078     * @param playbackRepeatSong whether the player is able to provide the info that the playback is repeating the song
079     * @param isUsingJava true if using java, false if not (and for example a C-library)
080     */
081    public Player(Context context, String id, boolean runsInPlay, List<Identifiable> activators,
082                  boolean providesTrackInfo, boolean playbackShuffle, boolean playbackRepeat,
083                  boolean playbackRepeatSong, boolean isUsingJava) {
084        super(context, id);
085        this.runsInPlay = runsInPlay;
086        this.activators = activators;
087        this.isUsingJava = isUsingJava;
088        capabilities = new Capabilities();
089        if (providesTrackInfo)
090            capabilities.setProvidesTrackInfo(true);
091        if (playbackShuffle)
092            capabilities.setPlaybackShuffle(true);
093        if (playbackRepeat)
094            capabilities.setPlaybackRepeat(true);
095        if (playbackRepeatSong)
096            capabilities.setPlaybackRepeatSong(true);
097        commandHandler = createCommandHandler();
098        informationProvider = new InformationProvider(getContext(), getID(), this,
099                commandHandler);
100        getContext().getEvents().registerEventListener(Collections.singletonList(LeavingEvent.ID), this);
101    }
102
103    /**
104     * creates a new output-plugin with a new id
105     *  @param context context
106     * @param id      the id of the new output-plugin. The ID of the InformationProvider is also based on this id
107     *                (id + ".InformationProvider")
108     * @param runsInPlay whether the termination of the play method should be treated as the termination the
109     *                   music (STOP not PAUSE)
110     * @param activator the activator which is able to start the Player if the Player is not able to start from
111     *                   request from other addons
112     * @param providesTrackInfo whether the Player provides TrackInfo
113     * @param playbackShuffle whether the player is able to provide the info that the playback is shuffling
114     * @param playbackRepeat whether the player is able to provide the info that the playback is repeating
115     * @param playbackRepeatSong whether the player is able to provide the info that the playback is repeating the song
116     * @param isUsingJava true if using java, false if not (and for example a C-library)
117     */
118    @SuppressWarnings("unused")
119    public Player(Context context, String id, boolean runsInPlay, Identifiable activator, boolean providesTrackInfo,
120                  boolean playbackShuffle, boolean playbackRepeat, boolean playbackRepeatSong, boolean isUsingJava) {
121        this(context, id, runsInPlay, Collections.singletonList(activator), providesTrackInfo,
122                playbackShuffle, playbackRepeat, playbackRepeatSong, isUsingJava);
123    }
124
125    /**
126     * creates a new output-plugin with a new id
127     *  @param context context
128     * @param id      the id of the new output-plugin
129     * @param runsInPlay whether the termination of the play method should be treated as the termination of playing the
130     *                   music
131     * @param playRequestTrackInfo whether the player is able to process PlayRequests with TrackInfo
132     * @param providesTrackInfo whether the Player provides TrackInfo
133     * @param playbackShuffle whether the player is able to provide the info that the playback is shuffling
134     * @param playbackRepeat whether the player is able to provide the info that the playback is repeating
135     * @param playbackRepeatSong whether the player is able to provide the info that the playback is repeating the song
136     * @param isUsingJava true if using java, false if not (and for example a C-library)
137     */
138    @SuppressWarnings("unused")
139    public Player(Context context, String id, boolean runsInPlay, boolean playRequestTrackInfo, boolean providesTrackInfo,
140                  boolean playbackShuffle, boolean playbackRepeat, boolean playbackRepeatSong, boolean isUsingJava) {
141        super(context, id);
142        this.runsInPlay = runsInPlay;
143        this.isUsingJava = isUsingJava;
144        activators = null;
145        capabilities = new Capabilities();
146        capabilities.setPlayRequestOutside(true);
147        if (playRequestTrackInfo)
148            capabilities.setPlayRequestDetailed(true);
149        if (providesTrackInfo)
150            capabilities.setProvidesTrackInfo(true);
151        if (playbackShuffle)
152            capabilities.setPlaybackShuffle(true);
153        if (playbackRepeat)
154            capabilities.setPlaybackRepeat(true);
155        if (playbackRepeatSong)
156            capabilities.setPlaybackRepeatSong(true);
157        commandHandler = createCommandHandler();
158        informationProvider = new InformationProvider(getContext(), getID(), this,
159                commandHandler);
160    }
161
162    /**
163     * returns the CommandHandler
164     * @return the CommandHandler
165     */
166    public CommandHandler getCommandHandler() {
167        return commandHandler;
168    }
169
170    /**
171     * true if playing and false if not
172     *
173     * @return tre if playing
174     */
175    @Override
176    public boolean isOutputRunning() {
177        if (runsInPlay) {
178            return playingThread != null && !playingThread.isDone();
179        } else {
180            return isRunning;
181        }
182    }
183
184    /**
185     * true if playing and false if not
186     *
187     * @return tre if playing
188     */
189    @Override
190    public boolean isPlaying() {
191        if (runsInPlay)
192            return isRunning;
193        return isPlaying;
194    }
195
196    /**
197     * true if using java, false if not (and for example a C-library)
198     *
199     * @return true if using java
200     */
201    @Override
202    public boolean isUsingJava() {
203        return isUsingJava;
204    }
205
206    /**
207     * stops the playing/indicates that the playing stopped.
208     * <p>
209     * this method has no effect if runsInPlay is enabled in the constructor.<br>
210     * </p>
211     */
212    public void stopMusicPlayback() {
213        if (runsInPlay || !isPlaying)
214            return;
215        lock.lock();
216        try {
217            if (blockRequest != null)
218                blockRequest.signal();
219        } finally {
220            lock.unlock();
221        }
222        isPlaying = false;
223        stopSound();
224        endedSound();
225        rollBackToDefault();
226        super.stop();
227    }
228
229    /**
230     * gets the current Playlist
231     *
232     * @return the current Playlist
233     */
234    @Override
235    public Playlist getCurrentPlaylist() {
236        return playlist;
237    }
238
239    /**
240     * fires an update event which notifies that parameters have changed
241     *
242     * @param playlist the optional playlist
243     */
244    @Override
245    public void updatePlayInfo(Playlist playlist) {
246        this.playlist = playlist;
247        MusicHelper.super.updatePlayInfo(playlist);
248    }
249
250    /**
251     * checks if the trackInfo is an update and fires the appropriate Event. This method should not be called
252     * without an active playlist, or an NullPointerException will be thrown.
253     * this method:<br>
254     *     - fires nothing if the trackInfo equals the current trackInfo.<br>
255     *     - fires an trackInfoUpdate if the trackInfo contains information not found in the current.<br>
256     */
257    @SuppressWarnings("unused")
258    public void updateCurrentTrackInfo(TrackInfo trackInfo) {
259        if (playlist.getCurrent().equals(trackInfo) ||
260                playlist.getCurrent().isNew(trackInfo))
261            return;
262        this.playlist = playlist.update(playlist.getCurrent(), trackInfo);
263        trackInfoUpdate(playlist, trackInfo);
264    }
265
266    /**
267     * call this method if the trackInfo object in the playlist was updated. Only the trackinfo object will be sent via
268     * Event
269     * @param playlist the playlist
270     * @param info the new trackInfo object
271     */
272    @SuppressWarnings("unused")
273    public void trackInfoUpdate(Playlist playlist, TrackInfo info) {
274        this.playlist = playlist;
275        updatePlayInfo(info);
276    }
277
278    /**
279     * gets the Volume
280     *
281     * @return the volume
282     */
283    @Override
284    public Volume getVolume() {
285        return volume;
286    }
287
288    /**
289     * fires an update event which notifies that parameters have changed
290     *
291     * @param volume the optional volume
292     */
293    @Override
294    public void updatePlayInfo(Volume volume) {
295        if (this.volume.equals(volume))
296            return;
297        this.volume = volume;
298        MusicHelper.super.updatePlayInfo(volume);
299    }
300
301    /**
302     * gets the Progress
303     *
304     * @return the Progress
305     */
306    @Override
307    public Progress getCurrentProgress() {
308        return progress;
309    }
310
311    /**
312     * fires an update event which notifies that parameters have changed
313     *
314     * @param progress the optional progress
315     */
316    @Override
317    public void updatePlayInfo(Progress progress) {
318        this.progress = progress;
319        MusicHelper.super.updatePlayInfo(progress);
320    }
321
322    /**
323     * gets the PlaybackState of the Player
324     *
325     * @return the PlaybackState
326     */
327    @Override
328    public PlaybackState getPlaybackState() {
329        return playbackState;
330    }
331
332    /**
333     * signals that the playing paused
334     */
335    @SuppressWarnings("unused")
336    public void pausePlaying() {
337        switch (playbackState) {
338            case PAUSE: return;
339            default: playbackState = PlaybackState.PAUSE;
340                updateStateInfo(playbackState);
341        }
342    }
343
344    /**
345     * signals that the playing resumed
346     */
347    @SuppressWarnings("unused")
348    public void resumePlaying() {
349        switch (playbackState) {
350            case PLAY: return;
351            default: playbackState = PlaybackState.PLAY;
352                updateStateInfo(playbackState);
353        }
354    }
355
356    /**
357     * updates the Info about the current song
358     * @param playlist the playlist, or null
359     * @param progress the progress, or null
360     * @param volume the volume, or null
361     */
362    @SuppressWarnings("unused")
363    public void updatePlayInfo(Playlist playlist, Progress progress, Volume volume) {
364        if (playlist != null)
365            this.playlist = playlist;
366        if (progress != null)
367            this.progress = progress;
368        if (volume != null)
369            this.volume = volume;
370        updatePlayInfo(playlist, progress, null, volume);
371    }
372
373    /**
374     * fires an update event which notifies that parameters have changed
375     *
376     * @param playlist  the optional playlist
377     * @param progress  the optional progress
378     * @param trackInfo the optional trackInfo
379     * @param volume    the optional volume
380     */
381    @Override
382    public void updatePlayInfo(Playlist playlist, Progress progress, TrackInfo trackInfo, Volume volume) {
383        if (playlist != null)
384            this.playlist = playlist;
385        if (progress != null)
386            this.progress = progress;
387        if (volume != null)
388            this.volume = volume;
389        MusicHelper.super.updatePlayInfo(playlist, progress, null, volume);
390    }
391
392    /**
393     * gets the Capabilities of the Player
394     *
395     * @return and instance of Capabilities
396     */
397    @Override
398    public Capabilities getCapabilities() {
399        return capabilities;
400    }
401
402    /**
403     * This method is called to register what resources the object provides.<br>
404     * just pass a List of Resources without Data in it.
405     *
406     * @return a List containing the resources the object provides
407     */
408    @Override
409    public List<? extends ResourceModel> announceResources() {
410        return informationProvider.announceResources();
411    }
412
413    /**
414     * this method is called to register for what Events it wants to provide Resources.
415     * <p>
416     * The Event has to be in the following format: It should contain only one Descriptor and and one Resource with the
417     * ID "description", which contains an description of the Event.
418     * </p>
419     *
420     * @return a List containing ID's for the Events
421     */
422    @Override
423    public List<? extends EventModel<?>> announceEvents() {
424        return informationProvider.announceEvents();
425    }
426
427    /**
428     * This method is called when an object wants to get a Resource.
429     * <p>
430     * Don't use the Resources provided as arguments, they are just the requests.
431     * There is a timeout after 1 second.
432     * </p>
433     *
434     * @param resources a list of resources without data
435     * @param event     if an event caused the action, it gets passed. It can also be null.
436     * @return a list of resources with data
437     */
438    @Override
439    public List<ResourceModel> provideResource(List<? extends ResourceModel> resources, Optional<EventModel> event) {
440        return informationProvider.provideResource(resources, event);
441    }
442
443    /**
444     * method that uses the data from the OutputExtensions to generate a final output that will then be rendered.
445     *
446     * @param data       the data generated
447     * @param eventModel the Event which caused the whole thing
448     */
449    @Override
450    public void renderFinalOutput(List<T> data, EventModel eventModel) {
451        if (StartMusicRequest.verify(eventModel, capabilities, this, activators)) {
452            if (isOutputRunning()) {
453                playerError(PlayerError.ERROR_ALREADY_PLAYING, eventModel.getSource());
454            } else {
455                handleEventRequest(eventModel);
456            }
457        } else if (eventModel.getListResourceContainer()
458                .providesResource(Collections.singletonList(MusicUsageResource.ID))){
459            if (isOutputRunning()) {
460                eventModel.getListResourceContainer()
461                        .provideResource(MusicUsageResource.ID)
462                        .forEach(resourceModel ->
463                                playerError(PlayerError.ERROR_ALREADY_PLAYING, resourceModel.getProvider()));
464            } else {
465                handleResourceRequest(eventModel);
466            }
467        } else {
468            handleCommands(eventModel);
469        }
470    }
471
472    /**
473     * handles the a request to start playing music via Resource
474     * @param eventModel the eventModel
475     */
476    private void handleResourceRequest(EventModel eventModel) {
477        if (MusicUsageResource.isPermanent(eventModel)) {
478            ResourceModel resourceModel = eventModel.getListResourceContainer()
479                    .provideResource(MusicUsageResource.ID)
480                    .stream()
481                    .filter(MusicUsageResource::isPermanent)
482                    .findAny()
483                    .orElse(null);//should not happen
484
485            //a partially applied function which takes an Identification an returns an Optional StartMusicRequest
486            Function<Identification, Optional<StartMusicRequest>> getStartMusicRequest = own ->
487                    StartMusicRequest.createStartMusicRequest(resourceModel.getProvider(), own);
488
489            //if we have a trackInfo we create it with the trackInfo as a parameter
490            getStartMusicRequest = TrackInfoResource.getTrackInfo(eventModel)
491                    .map(trackInfo -> (Function<Identification, Optional<StartMusicRequest>>) own ->
492                            StartMusicRequest.createStartMusicRequest(resourceModel.getProvider(), own, trackInfo))
493                    .orElse(getStartMusicRequest);
494
495            //if we have a trackInfo we create it with the playlist as a parameter
496            getStartMusicRequest = PlaylistResource.getPlaylist(eventModel)
497                    .map(playlist -> (Function<Identification, Optional<StartMusicRequest>>) own ->
498                            StartMusicRequest.createStartMusicRequest(resourceModel.getProvider(), own, playlist))
499                    .orElse(getStartMusicRequest);
500
501            //composes a new Function which appends the Volume to the result
502            getStartMusicRequest = getStartMusicRequest.andThen(
503                    VolumeResource.getVolume(eventModel)
504                    .flatMap(volume -> IdentificationManager.getInstance().getIdentification(this)
505                            .map(identification -> new VolumeResource(identification, volume)))
506                    .map(resource -> (Function<Optional<StartMusicRequest>, Optional<StartMusicRequest>>) opt ->
507                                    opt.map(event -> (StartMusicRequest) event.addResource(resource))
508                    )
509                    .orElse(Function.identity())::apply);
510
511            IdentificationManager.getInstance().getIdentification(this)
512                    .flatMap(getStartMusicRequest::apply)
513                    .ifPresent(this::fire);
514        } else {
515            play(eventModel);
516            if (!runsInPlay) {
517                blockRequest = lock.newCondition();
518                lock.lock();
519                try {
520                    blockRequest.await(10, TimeUnit.MINUTES);
521                } catch (InterruptedException e) {
522                    debug("interrupted", e);
523                } finally {
524                    lock.unlock();
525                }
526            }
527        }
528    }
529
530    /**
531     * handles the commands encoded as Resources/EventIds
532     * @param eventModel the eventModel to check
533     */
534    private void handleCommands(EventModel eventModel) {
535        Consumer<Runnable> checkOrCall = runnable -> {
536            List<ResourceModel> resourceModels = eventModel.getListResourceContainer()
537                    .provideResource(SelectorResource.RESOURCE_ID);
538            if (resourceModels.isEmpty()) {
539                runnable.run();
540            } else {
541                resourceModels.stream()
542                        .map(resourceModel -> resourceModel.getResource() instanceof Identification ?
543                                ((Identification) resourceModel.getResource()) : null)
544                        .filter(Objects::nonNull)
545                        .filter(this::isOwner)
546                        .findAny()
547                        .ifPresent(id -> runnable.run());
548            }
549        };
550        if (eventModel.containsDescriptor(MuteEvent.ID)) {
551            checkOrCall.accept(this::mute);
552        }
553        if (eventModel.containsDescriptor(UnMuteEvent.ID)) {
554            checkOrCall.accept(this::unMute);
555        }
556        if (eventModel.containsDescriptor(StopEvent.ID)) {
557            checkOrCall.accept(this::stopMusicPlayback);
558        }
559        if (StopMusic.verify(eventModel, this)) {
560            stopMusicPlayback();
561        }
562        if (PlayerCommand.verify(eventModel, this)) {
563            getCommandHandler().handleCommandResources(eventModel);
564        }
565    }
566
567    /**
568     * handles the a request to start playing music via Event
569     * @param eventModel the StartMusicRequest
570     */
571    private void handleEventRequest(EventModel eventModel) {
572        playingThread = submit((Runnable) () -> {
573                    //noinspection RedundantIfStatement
574                    if (runsInPlay) {
575                        isRunning = false;
576                    } else {
577                        isRunning = true;
578                    }
579                    isPlaying = true;
580                    fireStartMusicRequest(eventModel);
581                })
582                .thenRun(() -> play(eventModel))
583                .thenRun(() -> {
584                    if (runsInPlay) {
585                        isRunning = false;
586                        isPlaying = false;
587                        endedSound();
588                    }
589                });
590    }
591
592    /**
593     * override this method if you want to change the command handler
594     * @return the command handler
595     */
596    protected CommandHandler createCommandHandler() {
597        return new CommandHandler(this, this, this, capabilities);
598    }
599
600    /**
601     * this method will be called to create and fire the StartMusicRequest
602     * @param eventModel the cause
603     */
604    protected void fireStartMusicRequest(EventModel eventModel) {
605        Optional<Playlist> playlist = PlaylistResource.getPlaylist(eventModel);
606        Optional<Progress> progress = ProgressResource.getProgress(eventModel);
607        Optional<TrackInfo> trackInfo = TrackInfoResource.getTrackInfo(eventModel);
608        Optional<Volume> volume = VolumeResource.getVolume(eventModel);
609        startedSound(playlist.orElse(null), progress.orElse(null), trackInfo.orElse(null), volume.orElse(null), isUsingJava);
610    }
611
612    @Override
613    public void stop() {
614        stopMusicPlayback();
615    }
616
617    @Override
618    public void eventFired(EventModel eventModel) {
619        if (eventModel.containsDescriptor(LeavingEvent.GENERAL_DESCRIPTOR))
620            stopMusicPlayback();
621    }
622
623    /**
624     * sets every information into its default state (playlist, volume, etc...)
625     */
626    public void rollBackToDefault() {
627        playlist = new Playlist(new ArrayList<>());
628        volume = Volume.createVolume(50).orElse(null);
629        progress = new Progress(0,0);
630        playbackState = PlaybackState.PLAY;
631        if (playingThread != null)
632            playingThread.cancel(true);
633        isRunning = false;
634        isPlaying = false;
635    }
636
637    /**
638     * this method call must mute the plugin.
639     */
640    public abstract void mute();
641
642    /**
643     * this method call must un-mute the plugin.
644     */
645    public abstract void unMute();
646
647    /**
648     * this method call must stop the sound.<br>
649     * NEVER CALL THIS METHOD DIRECTLY, USE {@link #stopMusicPlayback()}.
650     */
651    public abstract void stopSound();
652
653    /**
654     * this method will be called if a request was cached which was eligible to start the music.<br>
655     * please check the events resources for parameters (if expected).
656     * @param eventModel the cause
657     */
658    public abstract void play(EventModel eventModel);
659}