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}